1#!/usr/bin/perl
2
3use YAML;
4use strict;
5use Data::Dumper;
6use File::Path;
7use Time::HiRes 'usleep';
8use Cwd;
9
10my $tmp_dir  = "/tmp";
11
12my %default_options = (
13    config             => 'conf/ngs.conf',
14    port               => 8080,
15    host               => '127.0.0.1',
16    bind               => '127.0.0.1',
17    worker_processes   => 5,
18    worker_connections => 1024,
19    access_log_path    => '/dev/stdout',
20    error_log_path     => '/dev/stdout',
21    nginx              => '/usr/local/nginx/sbin/nginx',
22);
23
24my $tmp_path = "$tmp_dir/td-$$";
25
26my $is_daemon = 0;
27
28$SIG{TERM} = \&cleanup;
29$SIG{INT}  = \&cleanup;
30
31# our primary dispatched sub
32sub run
33{
34    my $self = shift;
35    my $cwd  = cwd;
36
37    $self->parse_options;
38
39    # load yaml
40    $self->load_config;
41
42    for my $key (keys %default_options)
43    {
44        $self->{options}{$key} = $default_options{$key}
45        unless $self->{options}{$key} or $self->{config}{$key};
46    }
47
48    return $self->help           if $self->{options}{help};
49    return $self->list_processes if $self->{options}{list};
50    return $self->prune          if $self->{options}{prune};
51    return $self->stop           if $self->{options}{stop};
52
53    # hack for --single to work
54    $self->{options}{worker_processes} = 1
55        if $self->{options}{single};
56
57    # apache httpd holdover
58    $self->{options}{worker_processes} = 1
59        if $self->{options}{X};
60
61    # hack for --start to work
62    $self->{options}{daemon} = 1
63        if $self->{options}{start};
64
65    $self->prune
66        if $self->{options}{start};
67
68    return $self->write_nginx_config(dump => 1)
69        if $self->{options}{dump_config};
70
71    if ($self->config_option('daemon'))
72    {
73        my $basename = (split('/', $0))[-1];
74        warn "[$basename] - started as daemon\n";
75        exit(0) if fork;
76        $is_daemon = 1;
77    }
78
79    # reassert PID, in case we fork.
80    $tmp_path = "$tmp_dir/td-$$";
81    mkdir($tmp_path) unless -d $tmp_path;
82
83    my $config_file = $self->config_file;
84    my $cwd = ($config_file !~ /^\//) ? cwd : '';
85
86    open(MT, ">$tmp_path/config.path");
87    print MT "$cwd/$config_file\n";
88    close(MT);
89
90    open(MT, ">$tmp_path/td.pid");
91    print MT "$$\n";
92    close(MT);
93
94    # write mime types, if needed
95    $self->write_mime_types;
96    $self->write_nginx_config;
97
98    $self->start_nginx;
99
100    # wait! (no need to take up 100% of cpu)
101    while (getc()) {};
102}
103
104sub stop
105{
106    my $self = shift;
107
108    my @procs = $self->get_active_pids;
109    my $config_file = $self->config_file;
110    my $cwd         = ($config_file !~ /^\//) ? cwd : '';
111    my $full_config = "$cwd/$config_file";
112
113    my $killed = 0;
114    for my $proc (@procs)
115    {
116        $full_config =~ s{//}{/};
117
118        next unless $proc->{config_file} eq $full_config;
119        kill(15, $proc->{pid});
120        print "killed: [pid: $proc->{pid}] $proc->{config_file}\n";
121        $killed++;
122    }
123
124    $self->prune;
125
126    print "No active sessions.\n" unless $killed;
127}
128
129sub list_processes
130{
131    my $self = shift;
132
133    my @procs = $self->get_active_pids;
134
135    unless (@procs)
136    {
137        print "No active sessions.\n";
138        return;
139    }
140
141    print "Active sessions:\n";
142    for my $proc (@procs)
143    {
144        my $pid = $proc->{pid} || 'ZOMBIE';
145        print "  [pid: $pid] $proc->{config_file} [$proc->{dir}]\n";
146    }
147}
148
149sub get_active_pids
150{
151    my $self = shift;
152
153    opendir(DIR, $tmp_dir);
154    my @dirs = grep { -d "$tmp_dir/$_" and $_ =~ /^td-\d+$/ } readdir(DIR);
155    closedir(DIR);
156
157    my @pid_data;
158    for my $dir (@dirs)
159    {
160        # is this process running?
161        open (NGPID, "$tmp_dir/$dir/nginx.pid");
162        my $pid = <NGPID>;
163        close(NGPID);
164
165        # clense
166        $pid =~ s/[\n\r]//g;
167
168        next unless -d "/proc/$pid";
169
170        open(CF, "$tmp_dir/$dir/config.path");
171        my $config_location = <CF>;
172        close(CF);
173
174        chomp($config_location);
175
176        # avoid path starting with //
177        $config_location =~ s{//}{/};
178
179        push @pid_data, {
180            config_file => $config_location,
181            pid         => $pid,
182            dir         => "$tmp_dir/$dir/",
183        };
184    }
185
186    return @pid_data;
187}
188
189sub prune
190{
191    my $self = shift;
192
193    opendir(DIR, $tmp_dir);
194    my @dirs = grep { -d "$tmp_dir/$_" and $_ =~ /^td-\d+$/ } readdir(DIR);
195    closedir(DIR);
196
197    for my $dir (@dirs)
198    {
199        # is this process running?
200        open (NGPID, "$tmp_dir/$dir/nginx.pid");
201        my $pid = <NGPID>;
202        close(NGPID);
203
204        # clense
205        $pid =~ s/[\n\r]//g;
206
207        next if -d "/proc/$pid" and $pid;
208
209        open(CF, "$tmp_dir/$dir/nginx.pid");
210        my $nginx_pid = <CF>;
211        close(CF);
212
213        kill(15, $nginx_pid)
214            if $nginx_pid and -d "/proc/$nginx_pid";
215
216        rmtree("$tmp_dir/$dir");
217        print "Pruned: $tmp_dir/$dir\n";
218    }
219}
220
221sub cleanup
222{
223    my $self   = shift;
224    my $daemon = $is_daemon;
225
226    if (-e "$tmp_path/nginx.pid")
227    {
228        open(PID, "$tmp_path/nginx.pid");
229        my $pid = <PID>;
230        close(PID);
231
232        kill(15, $pid) if $pid;
233    }
234
235    unless ($daemon)
236    {
237        usleep(200000);
238    }
239
240    unlink("$tmp_path/nginx.conf");
241    unlink("$tmp_path/error.log");
242    unlink("$tmp_path/access.log");
243    unlink("$tmp_path/mime.types");
244    rmtree($tmp_path);
245
246    if ($daemon)
247    {
248        exit(0);
249    }
250    else
251    {
252        die "\n\nNginx stopped successfully.\n";
253    }
254}
255
256sub start_nginx
257{
258    my $self = shift;
259    my $path = $self->config_option('nginx');
260
261    my $daemon   = $self->config_option('daemon');
262    my $project  = $self->config_option('project');
263
264    warn "Starting $project...\n\n" if $project and not $daemon;
265
266    system($path, '-c', "$tmp_path/nginx.conf");
267
268    my $bind     = $self->config_option('bind');
269    my $ssl_port = $self->config_option('ssl_port');
270
271    my $o_bind = $bind;
272    $bind = 'localhost' if $bind eq 'all';
273
274    unless ($daemon)
275    {
276        my $port    = $self->config_option('port');
277        warn "Nginx is running on: http://$bind:$port/\n";
278        warn "  * Running with SSL on port $ssl_port.\n" if $ssl_port;
279        warn "  * Bound to all sockets.\n" if $o_bind eq 'all';
280        warn "\n";
281    }
282}
283
284sub config_option
285{
286    my ($self, $key) = @_;
287
288    my $config  = $self->{config};
289    my $options = $self->{options};
290
291    return $options->{$key} || $config->{$key} || '';
292}
293
294sub write_mime_types
295{
296    my $self = shift;
297
298    return if -e ">$tmp_path/mime.types";
299
300    my $txt = q[types {
301    text/html                             html htm shtml;
302    text/css                              css;
303    text/xml                              xml rss;
304    image/gif                             gif;
305    image/jpeg                            jpeg jpg;
306    application/x-javascript              js;
307    application/atom+xml                  atom;
308
309    text/mathml                           mml;
310    text/plain                            txt;
311    text/vnd.sun.j2me.app-descriptor      jad;
312    text/vnd.wap.wml                      wml;
313    text/x-component                      htc;
314
315    image/png                             png;
316    image/tiff                            tif tiff;
317    image/vnd.wap.wbmp                    wbmp;
318    image/x-icon                          ico;
319    image/x-jng                           jng;
320    image/x-ms-bmp                        bmp;
321    image/svg+xml                         svg;
322
323    application/java-archive              jar war ear;
324    application/mac-binhex40              hqx;
325    application/msword                    doc;
326    application/pdf                       pdf;
327    application/postscript                ps eps ai;
328    application/rtf                       rtf;
329    application/vnd.ms-excel              xls;
330    application/vnd.ms-powerpoint         ppt;
331    application/vnd.wap.wmlc              wmlc;
332    application/vnd.wap.xhtml+xml         xhtml;
333    application/x-cocoa                   cco;
334    application/x-java-archive-diff       jardiff;
335    application/x-java-jnlp-file          jnlp;
336    application/x-makeself                run;
337    application/x-perl                    pl pm;
338    application/x-pilot                   prc pdb;
339    application/x-rar-compressed          rar;
340    application/x-redhat-package-manager  rpm;
341    application/x-sea                     sea;
342    application/x-shockwave-flash         swf;
343    application/x-stuffit                 sit;
344    application/x-tcl                     tcl tk;
345    application/x-x509-ca-cert            der pem crt;
346    application/x-xpinstall               xpi;
347    application/zip                       zip;
348
349    application/octet-stream              bin exe dll;
350    application/octet-stream              deb;
351    application/octet-stream              dmg;
352    application/octet-stream              eot;
353    application/octet-stream              iso img;
354    application/octet-stream              msi msp msm;
355
356    audio/midi                            mid midi kar;
357    audio/mpeg                            mp3;
358    audio/x-realaudio                     ra;
359
360    video/3gpp                            3gpp 3gp;
361    video/mpeg                            mpeg mpg;
362    video/quicktime                       mov;
363    video/x-flv                           flv;
364    video/x-mng                           mng;
365    video/x-ms-asf                        asx asf;
366    video/x-ms-wmv                        wmv;
367    video/x-msvideo                       avi;
368}
369];
370
371    open(MT, ">$tmp_path/mime.types");
372    print MT $txt;
373    close(MT);
374}
375
376sub base_path
377{
378    my $self = shift;
379
380    return $self->config_option('root')
381        if $self->config_option('root');
382
383    my $config_file = $self->config_file;
384    my $cwd         = ($config_file !~ /^\//) ? cwd : '';
385    my $full_config = "$cwd/$config_file";
386
387    my @paths = split ('/', $full_config);
388    pop @paths; # rid of file name
389    pop @paths if $paths[-1] eq 'conf'; # rid of config directory
390
391    return join '/', @paths;
392}
393
394sub write_nginx_config
395{
396    my ($self, %params)    = @_;
397    my $base_path          = $self->base_path;
398    my $worker_processes   = $self->config_option('worker_processes');
399    my $worker_connections = $self->config_option('worker_connections');
400    my $require_module     = $self->config_option('require_module');
401    my $handler            = $self->config_option('handler');
402    my $app_path           = $self->config_option('app_path');
403
404    my @lj = grep { $_ } (
405        $self->config_option('bind'), $self->config_option('port')
406    );
407
408    $self->{options}{ssl_port} = $self->config_option('port') + 1000
409        if $self->config_option('ssl') and not $self->config_option('ssl_port');
410
411    # remove when bind is 'all'
412    shift @lj if $self->config_option('bind') eq 'all';
413
414    my $listen = join(':', @lj);
415    my $host   = $self->config_option('host');
416    my $port   = $self->config_option('port');
417
418    my @required = @{$self->{config}{require_modules} || [ ]};
419    my $perl_requires = '';
420    for my $req (@required)
421    {
422        $perl_requires .= "perl_require $req;\n";
423    }
424
425    my $locations = '';
426    my @locations = @{$self->{config}{locations} || [ ]};
427    for my $location (@locations)
428    {
429        if ($location->{handler})
430        {
431            $locations .= "\n";
432            $locations .= "\tlocation $location->{path} {\n";
433            $locations .= "\t\tperl $location->{handler};\n";
434            $locations .= "\t}\n";
435        }
436        else
437        {
438            $locations .= "\n";
439            $locations .= "\tlocation $location->{path} {\n";
440            $locations .= "\t\troot $base_path/$location->{root};\n";
441            $locations .= "\t\tindex $location->{index};\n";
442            $locations .= "\t}\n";
443        }
444    }
445
446    $locations .= $self->config_option('location_raw')
447        if $self->config_option('location_raw');
448
449    my $ssl_port = $self->config_option('ssl_port');
450    my $ssl = '';
451    if ($ssl_port)
452    {
453        my @slj = grep { $_ } (
454            $self->config_option('bind'), $self->config_option('ssl_port')
455        );
456
457        # remove when bind is 'all'
458        shift @slj if $self->config_option('bind') eq 'all';
459
460        my $ssl_listen = join(':', @slj);
461
462        $ssl = qq|
463    server {
464        listen       $ssl_listen;
465        set          \$is_ssl   1;
466        set          \$ssl_port $ssl_port;
467        server_name  $host;
468
469        error_page   404              /404.html;
470        error_page   500 502 503 504  /50x.html;
471
472        ssl                  on;
473        ssl_certificate      $base_path/ssl/cert.pem;
474        ssl_certificate_key  $base_path/ssl/cert.key;
475        ssl_session_timeout  5m;
476        ssl_protocols  SSLv2 SSLv3 TLSv1;
477        ssl_ciphers  ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
478        ssl_prefer_server_ciphers   on;
479        gzip on;
480
481        $locations
482    }
483|;
484    }
485
486    my $access_log_path = $self->config_option('access_log_path');
487    my $error_log_path = $self->config_option('error_log_path');
488
489    my $access_log = $self->config_option('access_log') eq 'off'
490        ? 'access_log  /dev/null  main;' : "access_log  $access_log_path  main;";
491
492    my $error_log = $self->config_option('error_log') eq 'off'
493        ? 'error_log  /dev/null;' : "error_log  $error_log_path;";
494
495    my $set_ssl_port = $ssl_port ? " set  \$ssl_port $ssl_port; " : '';
496
497    my $template = qq|
498worker_processes  $worker_processes;
499
500pid        $tmp_path/nginx.pid;
501
502$error_log
503
504events {
505    worker_connections  $worker_connections;
506}
507
508http {
509    include       $tmp_path/mime.types;
510    default_type  application/octet-stream;
511
512    log_format  main  '[\$time_local] \$remote_addr - \$request '
513                      '"\$status" \$body_bytes_sent';
514
515    $access_log
516
517    sendfile           on;
518    keepalive_timeout  65;
519
520    perl_modules $base_path/lib;
521
522    $perl_requires
523
524    server {
525        listen       $listen;
526        server_name  $host;
527        $set_ssl_port
528        gzip on;
529
530        error_page   404              /404.html;
531        error_page   500 502 503 504  /50x.html;
532
533        $locations
534    }
535
536    $ssl
537}
538|;
539
540    if ($params{dump})
541    {
542        print $template;
543    }
544    else
545    {
546        open(CT, ">$tmp_path/nginx.conf");
547        print CT $template;
548        close(CT);
549    }
550}
551
552sub load_config
553{
554    my $self = shift;
555    my $config_file = $self->config_file;
556
557    my $str_data;
558    open(CF, $config_file);
559    read(CF, $str_data, -s $config_file);
560    close(CF);
561
562    my $config = Load($str_data);
563
564    $self->{config} = $config;
565}
566
567sub config_file
568{
569    my $self = shift;
570    my $file = $self->{options}{config} || 'conf/ngs.conf';
571    if ($file)
572    {
573        $self->error("'$file' does not exist.")
574            unless -e $file;
575    }
576    else
577    {
578        $self->error("You must specify a config path.");
579    }
580
581    return $file;
582}
583
584sub error
585{
586    my ($self, $error) = @_;
587    my $basename = (split('/', $0))[-1];
588
589    die "[$basename] fatal error: $error\n";
590}
591
592sub help
593{
594    my $basename = (split('/', $0))[-1];
595    my $usage  = "usage: $basename [ options ]\n";
596    $usage    .= "  Options:\n";
597    $usage    .= "    --help                 (displays this message)\n";
598    $usage    .= "    --dump_config          (dumps generated nginx.conf only)\n";
599    $usage    .= "    --bind=[address|all]\n";
600    $usage    .= "    --config=path/to/conf  (default conf/ngs.conf)\n";
601    $usage    .= "    --port=[port]          (default 8080)\n";
602    $usage    .= "    --access_log=[on|off]  (default on)\n";
603    $usage    .= "    --error_log=[on|off]   (default on)\n";
604    $usage    .= "    --access_log_path=/pa  (default /dev/stdout)\n";
605    $usage    .= "    --error_log_path=/pa   (default /dev/stdout)\n";
606    $usage    .= "    --ssl                  (auto enable ssl on port 9080)\n";
607    $usage    .= "    --ssl_port=[port]      (enables ssl)\n";
608    $usage    .= "    --worker_processes=[#processes]\n";
609    $usage    .= "    --worker_connections=[#connections]\n";
610    $usage    .= "    --single               (only run one worker process)\n";
611    $usage    .= "    --nginx=/usr/local/nginx/sbin/nginx\n";
612    $usage    .= "  Daemon Mode:\n";
613    $usage    .= "    --daemon|--start       (default off)\n";
614    $usage    .= "    --list                 (list all active sessions)\n";
615    $usage    .= "    --prune                (cleanup all defunct sessions)\n";
616    $usage    .= "    --stop                 (stop session based on config)\n";
617
618    die $usage;
619}
620
621# quick and dirty
622sub parse_options
623{
624    my $self    = shift;
625
626    my %options;
627
628    my @acceptable_options = qw(
629        bind      port       access_log         error_log
630        ssl       ssl_port   worker_processes   single
631        nginx     help       worker_connections config
632        host      X          access_log_path    error_log_path
633        daemon    list       prune              stop
634        start     dump_config                   location_raw
635    );
636
637    for my $arg (@ARGV)
638    {
639        # cleanse all parameters of all unrighteousness
640        #   `--` & `-` any parameter shall be removed
641        $arg =~ s/^--//;
642        $arg =~ s/^-//;
643
644        # does this carry an assignment?
645        if ($arg =~ /=/)
646        {
647            my ($key, $value) = split('=', $arg);
648
649            $options{$key} = $value;
650        }
651        else
652        {
653            $options{$arg} = 1;
654        }
655    }
656
657    for my $option (keys %options)
658    {
659        $self->error("`$option` is an invalid option")
660            unless (grep { $_ eq $option } @acceptable_options)
661    }
662
663    $self->{options} = \%options;
664
665    return \%options;
666}
667
668# BANG!
669my $run = {};
670bless($run);
671$run->run;
672
6731;
674