# Common functions for Nginx config file
use strict;
use warnings;
no warnings 'recursion';
use Socket;
BEGIN { push(@INC, ".."); };
eval "use WebminCore;";
&init_config();
our %access = &get_module_acl();
our ($get_config_cache, $get_config_parent_cache, %list_directives_cache,
@list_modules_cache, @open_config_files);
our (%config, %text, %in, $module_root_directory);
my @lock_all_config_files_cache;
# get_config()
# Parses the Nginx config file into an array ref
sub get_config
{
if (!$get_config_cache) {
$get_config_cache = &read_config_file($config{'nginx_config'});
}
return $get_config_cache;
}
# get_config_parent()
# Returns an object that represents the whole config file
sub get_config_parent
{
if (!$get_config_parent_cache) {
$get_config_parent_cache = { 'members' => &get_config(),
'type' => 1,
'file' => $config{'nginx_config'},
'indent' => -1,
'line' => 0,
'eline' => 0 };
foreach my $c (@{$get_config_parent_cache->{'members'}}) {
if ($c->{'file'} eq $get_config_parent_cache->{'file'} &&
$c->{'eline'} > $get_config_parent_cache->{'eline'}) {
$get_config_parent_cache->{'eline'} = $c->{'eline'}+1;
}
}
}
return $get_config_parent_cache;
}
# flush_config_cache()
# Delete all in-memory config caches
sub flush_config_cache
{
undef($get_config_parent_cache);
undef($get_config_cache);
}
# remove_hash_comment(line)
# Returns the line with comments removed
sub remove_hash_comment
{
my ($l) = @_;
if ($l =~ /".*#.*"/) {
# Comment inside quotes, so only remove any comment outside quotes
$l =~ s/#[^"]*$//;
}
else {
# Remove all comments
$l =~ s/#.*$//;
}
return $l;
}
# read_config_file(file, [preserve-includes])
# Returns an array ref of nginx config objects
sub read_config_file
{
my ($file, $noinc) = @_;
my $link = &resolve_links($file);
$link || &error("Dangling link $file");
$file = $link;
my @rv = ( );
my $addto = \@rv;
my @stack = ( );
my $lnum = 0;
my $fh = "CFILE".int(rand(1000000));
&open_readfile($fh, $file) || return [];
my @lines = <$fh>;
close($fh);
while(@lines) {
my $l = shift(@lines);
$l = &remove_hash_comment($l);
my $slnum = $lnum;
# If line doesn't end with { } or ; , it must be continued on the
# next line
while($l =~ /\S/ && $l !~ /[\{\}\;]\s*$/ && @lines) {
my $nl = shift(@lines);
if ($nl =~ /\S/) {
$nl = &remove_hash_comment($nl);
$l .= " ".$nl;
}
$lnum++;
}
if ($l =~ /^\s*if\s*\((.*)\)\s*\{\s*$/) {
# Start of an if statement
my $ns = { 'name' => 'if',
'type' => 2,
'indent' => scalar(@stack),
'file' => $file,
'line' => $slnum,
'eline' => $lnum,
'members' => [ ] };
my $value = $1;
&set_split_words($ns, " ".$value);
push(@stack, $addto);
push(@$addto, $ns);
$addto = $ns->{'members'};
}
elsif ($l =~ /^\s*(\S+)(\s*.*)\{\s*$/) {
# Start of a section
my $ns = { 'name' => $1,
'type' => 1,
'indent' => scalar(@stack),
'file' => $file,
'line' => $slnum,
'eline' => $lnum,
'members' => [ ] };
my $value = $2;
&set_split_words($ns, $value);
push(@stack, $addto);
push(@$addto, $ns);
$addto = $ns->{'members'};
}
elsif ($l =~ /^\s*}/ && @stack) {
# End of a section
$addto = pop(@stack);
$addto->[@$addto-1]->{'eline'} = $lnum;
}
elsif ($l =~ /^\s*(\S+)((\s+("([^"]*)"|'([^']*)'|[^ ;]+))*)\s*;/) {
# Found a directive
my ($name, $value) = ($1, $2);
my @words = &split_words($value);
if ($name eq "include" && !$noinc) {
# Include a file or glob
if ($words[0] !~ /^\//) {
my $filedir = $file;
$filedir =~ s/\/[^\/]+$//;
$words[0] = $filedir."/".$value;
}
foreach my $ifile (glob($words[0])) {
my $inc = &read_config_file($ifile);
push(@$addto, @$inc);
}
}
else {
# Some directive in the current section
my $dir = { 'name' => $name,
'value' => $words[0],
'words' => \@words,
'type' => 0,
'file' => $file,
'line' => $slnum,
'eline' => $lnum };
push(@$addto, $dir);
if (@stack) {
my $lastaddto = $stack[$#stack];
$lastaddto->[@$lastaddto - 1]->{'eline'} = $lnum;
}
}
}
elsif ($l =~ /\S/) {
$l =~ s/\r|\n//g;
print STDERR "Invalid Nginx config line $l at $lnum in $file\n";
}
$lnum++;
}
return \@rv;
}
# split_words(string)
# Convert a string of bare or quoted words into a list
sub split_words
{
my ($value) = @_;
my @words;
while($value =~ s/^\s+"([^"]+)"// ||
$value =~ s/^\s+'([^']+)'// ||
$value =~ s/^\s+(\S+)//) {
push(@words, $1);
}
return @words;
}
# set_split_words(&directive, string)
# Set the words and value fields based on a string
sub set_split_words
{
my ($ns, $value) = @_;
my @s = &split_words($value);
$ns->{'words'} = \@s;
$ns->{'value'} = @s ? $s[0] : undef;
}
# get_add_to_file(name)
# Returns the file to add new servers to, if any
sub get_add_to_file
{
my ($name) = @_;
if (!$config{'add_to'}) {
return undef;
}
elsif (-d $config{'add_to'}) {
$name =~ s/[^a-zA-Z0-9\.\_\-]//g;
if ($name) {
return $config{'add_to'}."/".$name.".conf";
}
}
else {
return $config{'add_to'};
}
return undef;
}
# find(name, [&config|&parent])
# Returns the object or objects with some name in the given config
sub find
{
my ($name, $conf) = @_;
$conf ||= &get_config();
if (ref($conf) eq 'HASH') {
$conf = $conf->{'members'};
}
my @rv;
foreach my $c (@$conf) {
if (lc($c->{'name'}) eq $name) {
push(@rv, $c);
}
}
return wantarray ? @rv : $rv[0];
}
# find_value(name, [config])
# Returns the value of the object or objects with some name in the given config
sub find_value
{
my ($name, $conf) = @_;
my @rv = map { my @w = @{$_->{'words'}};
(@w ? $w[0] : undef) || $_->{'value'} } &find($name, $conf);
return wantarray ? @rv : $rv[0];
}
# find_recursive(name, [&config|&parent])
# Returns all objects under some parent with the given name
sub find_recursive
{
my ($name, $conf) = @_;
$conf ||= &get_config();
if (ref($conf) eq 'HASH') {
$conf = $conf->{'members'};
}
my @rv;
foreach my $c (@$conf) {
if (lc($c->{'name'}) eq $name) {
push(@rv, $c);
}
if ($c->{'type'}) {
push(@rv, &find_recursive($name, $c));
}
}
return wantarray ? @rv : $rv[0];
}
# save_directive(&parent, name|&oldobjects, &newvalues|&newobjects, [&before])
# Updates the values of some named directive
sub save_directive
{
my ($parent, $name_or_oldstructs, $values, $before) = @_;
$values = [ $values ] if (!ref($values));
my $oldstructs = ref($name_or_oldstructs) ? $name_or_oldstructs :
[ &find($name_or_oldstructs, $parent) ];
my $name = !ref($name_or_oldstructs) ? $name_or_oldstructs :
@$name_or_oldstructs ? $name_or_oldstructs->[0]->{'name'} : undef;
my $newstructs = [ map { &value_to_struct($name, $_) } @$values ];
for(my $i=0; $i<@$newstructs || $i<@$oldstructs; $i++) {
my $o = $i<@$oldstructs ? $oldstructs->[$i] : undef;
my $n = $i<@$newstructs ? $newstructs->[$i] : undef;
my $file = $o ? $o->{'file'} :
$n && $n->{'file'} ? $n->{'file'} : $parent->{'file'};
my $lref = &read_file_lines($file);
push(@open_config_files, $file);
if ($i<@$newstructs && $i<@$oldstructs) {
# Updating some directive
my $olen = $o->{'eline'} - $o->{'line'} + 1;
my @lines = &make_directive_lines($n, $parent->{'indent'}+1);
$o->{'name'} = $n->{'name'};
$o->{'value'} = $n->{'words'}->[0];
$o->{'words'} = $n->{'words'};
splice(@$lref, $o->{'line'}, $olen, @lines);
if ($olen != scalar(@lines)) {
# Renumber directives
&renumber($file, $o->{'line'}, $olen - scalar(@lines));
$o->{'eline'} = $o->{'line'} + scalar(@lines) - 1;
}
}
elsif ($i<@$newstructs) {
# Adding a directive
my @lines;
$n->{'value'} = $n->{'words'}->[0];
if ($n->{'file'}) {
# New file, add at start
@lines = &make_directive_lines($n, 0);
$n->{'line'} = 0;
$n->{'eline'} = scalar(@lines) - 1;
$n->{'indent'} = 0 if ($n->{'type'});
&recursive_set_file($n, $n->{'file'}, $n->{'line'});
unshift(@{$parent->{'members'}}, $n);
}
elsif ($before) {
# Insert into parent before some other directive
@lines = &make_directive_lines(
$n, $parent->{'indent'} + 1);
$n->{'line'} = $before->{'line'};
$n->{'eline'} = $n->{'line'} + scalar(@lines) - 1;
&recursive_set_file($n, $file, $n->{'line'});
&renumber($file, $n->{'line'}-1, scalar(@lines));
$n->{'indent'} = $parent->{'indent'} + 1
if ($n->{'type'});
my $idx = &indexof($before, @{$parent->{'members'}});
if ($idx >= 0) {
splice(@{$parent->{'members'}}, $idx, 0, $n);
}
else {
push(@{$parent->{'members'}}, $n);
}
}
else {
# Insert into parent at end
@lines = &make_directive_lines(
$n, $parent->{'indent'} + 1);
$n->{'line'} = $parent->{'eline'};
$n->{'eline'} = $n->{'line'} + scalar(@lines) - 1;
&recursive_set_file($n, $file, $n->{'line'});
&renumber($file, $parent->{'eline'}-1, scalar(@lines));
$n->{'indent'} = $parent->{'indent'} + 1
if ($n->{'type'});
push(@{$parent->{'members'}}, $n);
}
splice(@$lref, $n->{'line'}, 0, @lines);
}
elsif ($i<@$oldstructs) {
# Removing a directive
my $olen = $o->{'eline'} - $o->{'line'} + 1;
splice(@$lref, $o->{'line'}, $olen);
my $idx = &indexof($o, @{$parent->{'members'}});
if ($idx >= 0) {
splice(@{$parent->{'members'}}, $idx, 1);
}
&renumber($file, $o->{'line'}, -$olen);
}
}
}
# renumber(filename, line, offset, [&parent])
# Adjusts the line number of any directive after the one given by the offset
sub renumber
{
my ($file, $line, $offset, $object) = @_;
$object ||= &get_config_parent();
if ($object->{'file'} eq $file) {
$object->{'line'} += $offset if ($object->{'line'} > $line);
$object->{'eline'} += $offset if ($object->{'eline'} > $line);
}
if ($object->{'type'}) {
foreach my $m (@{$object->{'members'}}) {
&renumber($file, $line, $offset, $m);
}
}
}
# recursive_set_file(&parent, filename, start-line)
# Sets the file on some object and all children
sub recursive_set_file
{
my ($parent, $file, $line) = @_;
$parent->{'file'} ||= $file;
$parent->{'line'} ||= $line;
$parent->{'eline'} ||= $parent->{'line'};
if ($parent->{'type'}) {
my $n = 1;
foreach my $dir (@{$parent->{'members'}}) {
&recursive_set_file($dir, $file, $parent->{'line'} + $n);
$n += ($dir->{'eline'} - $dir->{'line'} + 1);
}
$parent->{'eline'} = $parent->{'line'} + $n;
}
}
# flush_config_file_lines([&parent])
# Flush all lines in the current config
sub flush_config_file_lines
{
my ($parent) = @_;
foreach my $f (&unique(@open_config_files)) {
&flush_file_lines($f);
}
@open_config_files = ( );
}
# lock_all_config_files([&parent])
# Locks all files used in the current config
sub lock_all_config_files
{
my ($parent) = @_;
@lock_all_config_files_cache = &get_all_config_files($parent);
foreach my $f (@lock_all_config_files_cache) {
&lock_file($f);
}
}
# unlock_all_config_files([&parent])
# Un-locks all files used in the current config
sub unlock_all_config_files
{
my ($parent) = @_;
foreach my $f (reverse(@lock_all_config_files_cache)) {
&unlock_file($f);
}
@lock_all_config_files_cache = ();
}
# get_all_config_files([&parent])
# Returns all files in the given config object
sub get_all_config_files
{
my ($parent) = @_;
$parent ||= &get_config_parent();
my @rv = ( $parent->{'file'} );
if ($parent->{'type'}) {
foreach my $c (@{$parent->{'members'}}) {
push(@rv, &get_all_config_files($c));
}
}
return &unique(@rv);
}
# make_directive_lines(&directive, indent)
# Returns text for some directive
sub make_directive_lines
{
my ($dir, $indent) = @_;
my @rv;
my @w = @{$dir->{'words'}};
if ($dir->{'type'}) {
# Multi-line
if ($dir->{'name'} eq 'if') {
push(@rv, $dir->{'name'}.' ('.&join_words(@w).') {');
}
else {
push(@rv, $dir->{'name'}.(@w ? " ".&join_words(@w) : "")." {");
}
foreach my $m (@{$dir->{'members'}}) {
push(@rv, &make_directive_lines($m, 1));
}
push(@rv, "}");
}
else {
# Single line
push(@rv, $dir->{'name'}." ".&join_words(@w).";");
}
foreach my $r (@rv) {
$r = ("\t" x $indent).$r;
}
return wantarray ? @rv : $rv[0];
}
# join_words(word, etc..)
# Returns a string made by joining directive words
sub join_words
{
my @rv;
foreach my $w (@_) {
if ($w eq "") {
push(@rv, '""');
}
elsif ($w =~ /\s|;|\$/ && $w !~ /"/ && $w !~ /^\$/) {
push(@rv, "\"$w\"");
}
elsif ($w =~ /\s|;|\$/ && $w !~ /^\$/) {
push(@rv, "'$w'");
}
else {
push(@rv, $w);
}
}
return join(" ", @rv);
}
# value_to_struct(name, value)
# Converts a string, array ref or hash ref to a config struct
sub value_to_struct
{
my ($name, $value) = @_;
if (ref($value) eq 'HASH') {
# Already in correct format
$value->{'name'} ||= $name;
return $value;
}
elsif (ref($value) eq 'ARRAY') {
# Array of words
return { 'name' => $name,
'words' => $value,
'value' => $value->[0] };
}
else {
# Single value
return { 'name' => $name,
'words' => [ $value ],
'value' => $value };
}
}
# get_nginx_version()
# Returns the version number of the installed Nginx binary
sub get_nginx_version
{
my $out = &backquote_command("$config{'nginx_cmd'} -v 2>&1 $module,
'name' => $name,
'default' => $default eq '-' ? undef : $default,
'context' => $context eq '-' ? undef :
[ split(/,/, $context) ],
};
}
}
return \%list_directives_cache;
}
# get_default(name)
# Returns the default value for some directive
sub get_default
{
my ($name) = @_;
my $dirs = &list_nginx_directives();
my $dir = $dirs->{$name};
return $dir ? $dir->{'default'} : undef;
}
sub get_default_server_param
{
my $ver = &get_nginx_version();
return &compare_version_numbers($ver, "0.8.21") >= 0 ?
"default_server" : "default";
}
# list_nginx_modules()
# Returns a list of enabled modules. Includes those compiled in by default
# unless disabled, plus extra compiled in at build time.
sub list_nginx_modules
{
if (!@list_modules_cache) {
@list_modules_cache = ( 'http_core', 'http_access', 'http_access',
'http_auth_basic', 'http_autoindex',
'http_browser', 'http_charset',
'http_empty_gif', 'http_fastcgi', 'http_geo',
'http_gzip', 'http_limit_req',
'http_limit_zone', 'http_map',
'http_memcached', 'http_proxy',
'http_referer', 'http_rewrite',
'http_scgi', 'http_split_clients',
'http_ssi', 'http_userid', 'http_index',
'http_uwsgi', 'http_log', 'core' );
my $out = &backquote_command("$config{'nginx_cmd'} -V 2>&1 {$name};
return 0 if (!$dir);
return 0 if ($dir->{'context'} && $parent &&
&indexof($parent->{'name'}, @{$dir->{'context'}}) < 0);
my @mods = &list_nginx_modules();
#return 0 if (&indexof($dir->{'module'}, @mods) < 0);
return 1;
}
# nginx_onoff_input(name, &parent)
# Returns HTML for a table row for an on/off input
sub nginx_onoff_input
{
my ($name, $parent) = @_;
return undef if (!&supported_directive($name, $parent));
my $value = &find_value($name, $parent);
$value ||= &get_default($name);
$value ||= "";
return &ui_table_row($text{'opt_'.$name},
&ui_yesno_radio($name, $value =~ /on|true|yes/i ? 1 : 0));
}
# nginx_onoff_parse(name, &parent, &in)
# Updates the config with input from nginx_onoff_input
sub nginx_onoff_parse
{
my ($name, $parent, $in) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
&save_directive($parent, $name, [ $in->{$name} ? "on" : "off" ]);
}
# nginx_opt_input(name, &parent, size, prefix, suffix, [multi-value])
# Returns HTML for an optional text field
sub nginx_opt_input
{
my ($name, $parent, $size, $prefix, $suffix, $multi) = @_;
return undef if (!&supported_directive($name, $parent));
my $obj = &find($name, $parent);
my $value = $multi ? &join_words(@{$obj->{'words'}}) : $obj->{'value'};
my $def = &get_default($name);
return &ui_table_row($text{'opt_'.$name},
&ui_opt_textbox($name, $value, $size,
$text{'default'}.($def ? " ($def)" : ""), $prefix).
$suffix, $size > 40 ? 3 : 1);
}
# nginx_opt_parse(name, &parent, &in, [regex], [&validator], [multi-value])
# Updates the config with input from nginx_opt_input
sub nginx_opt_parse
{
my ($name, $parent, $in, $regexp, $vfunc, $multi) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
if ($in->{$name."_def"}) {
&save_directive($parent, $name, [ ]);
}
else {
my $v = $in->{$name};
my @w = $multi ? &split_quoted_string($v) : ( $v );
$v eq '' && &error(&text('opt_missing', $text{'opt_'.$name}));
!$regexp || $v =~ /$regexp/ || &error($text{'opt_e'.$name} || $name);
my $err = $vfunc && &$vfunc($v, $name);
$err && &error($err);
&save_directive($parent, $name, [ { 'name' => $name,
'words' => \@w } ]);
}
}
# nginx_text_input(name, &parent, size, suffix, [multi-value])
# Returns HTML for a non-optional text field
sub nginx_text_input
{
my ($name, $parent, $size, $suffix, $multi) = @_;
return undef if (!&supported_directive($name, $parent));
my $obj = &find($name, $parent);
my $value = $multi ? &join_words(@{$obj->{'words'}}) : $obj->{'value'};
$suffix ||= "";
return &ui_table_row($text{'opt_'.$name},
&ui_textbox($name, $value, $size).$suffix, $size > 40 ? 3 : 1);
}
# nginx_text_parse(name, &parent, &in, [regex], [&validator], [multi-value])
# Updates the config with input from nginx_text_input
sub nginx_text_parse
{
my ($name, $parent, $in, $regexp, $vfunc, $multi) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
my $v = $in->{$name};
my @w = $multi ? &split_quoted_string($v) : ( $v );
foreach my $wv (@w) {
$wv eq '' && &error(&text('opt_missing', $text{'opt_'.$name}));
!$regexp || $wv =~ /$regexp/ || &error($text{'opt_e'.$name});
my $err = $vfunc && &$vfunc($wv, $name);
$err && &error($err);
}
&save_directive($parent, $name, [ { 'name' => $name,
'words' => \@w } ]);
}
# nginx_error_log_input(name, &parent)
# Returns HTML specifically for setting the error_log directive
sub nginx_error_log_input
{
my ($name, $parent) = @_;
return undef if (!&supported_directive($name, $parent));
my $obj = &find($name, $parent);
my $def = $parent->{'name'} eq 'server' ? $text{'opt_global'}
: &get_default($name);
$def =~ s/^\$\{prefix\}\///;
return &ui_table_row($text{'opt_'.$name},
&ui_radio($name."_def", $obj ? 0 : 1,
[ [ 1, $text{'default'}.($def ? " ($def)" : "")."
" ],
[ 0, $text{'logs_file'} ] ])." ".
&ui_textbox($name, $obj ? $obj->{'words'}->[0] : undef, 40)." ".
$text{'logs_level'}." ".
&ui_select($name."_level", $obj ? $obj->{'words'}->[1] : "",
[ [ "", "<$text{'default'}>" ],
"debug", "info", "notice", "warn", "error", "crit" ]));
}
# nginx_error_log_parse(name, &parent, &in)
# Validate input from nginx_error_log_input
sub nginx_error_log_parse
{
my ($name, $parent, $in) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
if ($in->{$name."_def"}) {
&save_directive($parent, $name, [ ]);
}
else {
$in->{$name} || &error(&text('opt_missing', $text{'opt_'.$name}));
$in->{$name} =~ /^\/\S+$/ || &error($text{'opt_e'.$name});
my @w = ( $in->{$name} );
push(@w, $in->{$name."_level"}) if ($in->{$name."_level"});
&save_directive($parent, $name, [ { 'name' => $name,
'words' => \@w } ]);
}
}
# nginx_access_log_input(name, &parent)
# Returns HTML specifically for setting the access_log directive
sub nginx_access_log_input
{
my ($name, $parent) = @_;
return undef if (!&supported_directive($name, $parent));
my $obj = &find($name, $parent);
my $mode = !$obj ? 1 : $obj->{'value'} eq 'off' ? 2 : 0;
my $buffer = $mode == 0 && $obj->{'words'}->[2] =~ /buffer=(\S+)/ ? $1 : "";
my $def = $parent->{'name'} eq 'server' ? $text{'opt_global'}
: &get_default($name);
return &ui_table_row($text{'opt_'.$name},
&ui_radio($name."_def", $mode,
[ [ 1, $text{'default'}.($def ? " ($def)" : "")."
" ],
[ 2, $text{'logs_disabled'}."
" ],
[ 0, $text{'logs_file'} ] ])." ".
&ui_textbox($name, $mode == 0 ? $obj->{'words'}->[0] : undef, 40)." ".
$text{'logs_format'}." ".
&ui_select($name."_format", $mode == 0 ? $obj->{'words'}->[1] : "",
[ [ "", "<$text{'default'}>" ],
&list_log_formats($parent) ])." ".
$text{'logs_buffer'}." ".
&ui_textbox($name."_buffer", $buffer, 6));
}
# nginx_access_log_parse(name, &parent, &in)
# Validate input from nginx_access_log_input
sub nginx_access_log_parse
{
my ($name, $parent, $in) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
if ($in->{$name."_def"} == 1) {
&save_directive($parent, $name, [ ]);
}
elsif ($in->{$name."_def"} == 2) {
&save_directive($parent, $name, [ "off" ]);
}
else {
$in->{$name} || &error(&text('opt_missing', $text{'opt_'.$name}));
$in->{$name} =~ /^\/\S+$/ || &error($text{'opt_e'.$name});
my @w = ( $in->{$name} );
push(@w, $in->{$name."_format"}) if ($in->{$name."_format"});
my $buffer = $in->{$name."_buffer"};
if ($buffer) {
$buffer =~ /^\d+[bKMGT]?$/i || &error($text{'logs_ebuffer'});
push(@w, "buffer=$buffer");
}
&save_directive($parent, $name, [ { 'name' => $name,
'words' => \@w } ]);
}
}
# nginx_user_input(name, &parent)
# Returns HTML for a user field with an optional group
sub nginx_user_input
{
my ($name, $parent) = @_;
return undef if (!&supported_directive($name, $parent));
my $obj = &find($name, $parent);
my $def = &get_default($name);
return &ui_table_row($text{'opt_'.$name},
&ui_radio($name."_def", $obj ? 0 : 1,
[ [ 1, $text{'default'}.($def ? " ($def)" : "")."
" ],
[ 0, $text{'misc_username'} ] ])." ".
&ui_user_textbox($name, $obj ? $obj->{'words'}->[0] : "")." ".
$text{'misc_group'}." ".
&ui_group_textbox($name."_group", $obj ? $obj->{'words'}->[1] : ""));
}
# nginx_user_parse(name, &parent, &in)
# Validate input from nginx_user_input
sub nginx_user_parse
{
my ($name, $parent, $in) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
if ($in->{$name."_def"} == 1) {
&save_directive($parent, $name, [ ]);
}
else {
$in->{$name} || &error(&text('opt_missing', $text{'opt_'.$name}));
defined(getpwnam($in->{$name})) || &error($text{'misc_euser'});
my @w = ( $in->{$name} );
my $group = $in->{$name."_group"};
if ($group) {
defined(getgrnam($group)) || &error($text{'misc_egroup'});
push(@w, $group);
}
&save_directive($parent, $name, [ { 'name' => $name,
'words' => \@w } ]);
}
}
# nginx_logformat_input(name, parent)
# Returns HTML for entering multiple log formats
sub nginx_logformat_input
{
my ($name, $parent) = @_;
return undef if (!&supported_directive($name, $parent));
my @obj = &find($name, $parent);
my $ftable = &ui_columns_start([ $text{'logs_fname'},
$text{'logs_ftext'} ]);
my $i = 0;
foreach my $o (@obj, { 'words' => [ ] }) {
my @w = @{$o->{'words'}};
$ftable .= &ui_columns_row([
&ui_textbox($name."_name_$i", shift(@w), 20),
&ui_textbox($name."_text_$i", join(" ", @w), 60),
]);
$i++;
}
$ftable .= &ui_columns_end();
return &ui_table_row($text{'opt_'.$name}, $ftable, 3);
}
# nginx_logformat_parse(name, &parent, &in)
# Validate input from nginx_logformat_input
sub nginx_logformat_parse
{
my ($name, $parent, $in) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
my @obj;
for(my $i=0; defined(my $fname = $in{$name."_name_$i"}); $i++) {
next if (!$fname);
my $ftext = $in{$name."_text_$i"};
$fname =~ /^[a-zA-Z0-9\-\.\_]+$/ ||
&error(&text('logs_efname', $fname));
$ftext =~ /\S/ || &error(&text('logs_etext', $fname));
push(@obj, { 'name' => $name,
'words' => [ $fname, $ftext ] });
}
&save_directive($parent, $name, \@obj);
}
# nginx_multi_input(name, &parent, &options)
# Returns HTML for selecting multiple options
sub nginx_multi_input
{
my ($name, $parent, $opts) = @_;
return undef if (!&supported_directive($name, $parent));
my $def = &get_default($name);
my $obj = &find($name, $parent);
return &ui_table_row($text{'opt_'.$name},
&ui_radio($name."_def", $obj ? 0 : 1,
[ [ 1, $text{'default'}.($def ? " ($def)" : "") ],
[ 0, $text{'opt_selected'}."
" ] ])." ".
&ui_select($name, $obj ? $obj->{'words'} : [ ], $opts, scalar(@$opts),
1, 1));
}
# nginx_multi_parse(name, &parent)
# Validate input from nginx_multi_input
sub nginx_multi_parse
{
my ($name, $parent, $in) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
if ($in->{$name."_def"} == 1) {
&save_directive($parent, $name, [ ]);
}
else {
my @w = split(/\0/, $in->{$name});
@w || &error(&text('opt_missing', $text{'opt_'.$name}));
&save_directive($parent, $name, [ { 'name' => $name,
'words' => \@w } ]);
}
}
# nginx_param_input(name, &parent, [name-text, value-text])
# Returns HTML for entering multiple name value paramters
sub nginx_param_input
{
my ($name, $parent, $ntext, $vtext) = @_;
$ntext ||= $text{'fcgi_pname'};
$vtext ||= $text{'fcgi_pvalue'};
return undef if (!&supported_directive($name, $parent));
my @obj = &find($name, $parent);
my $ftable = &ui_columns_start([ $ntext, $vtext ]);
my $i = 0;
foreach my $o (@obj, { 'words' => [ ] }) {
my @w = @{$o->{'words'}};
$ftable .= &ui_columns_row([
&ui_textbox($name."_name_$i", shift(@w), 20),
&ui_textbox($name."_value_$i", join(" ", @w), 60),
]);
$i++;
}
$ftable .= &ui_columns_end();
return &ui_table_row($text{'opt_'.$name}, $ftable, 3);
}
# nginx_params_parse(name, &parent, &in)
# Parses inputs from nginx_param_input
sub nginx_params_parse
{
my ($name, $parent, $in) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
my @obj;
for(my $i=0; defined(my $pname = $in{$name."_name_$i"}); $i++) {
next if (!$pname);
my $pvalue = $in{$name."_value_$i"};
$pname =~ /^[a-zA-Z0-9\-\.\_]+$/ ||
&error(&text('fcgi_epname', $pname));
$pvalue =~ /\S/ || &error(&text('fcgi_epvalue', $pname));
push(@obj, { 'name' => $name,
'words' => [ $pname, $pvalue ] });
}
&save_directive($parent, $name, \@obj);
}
# nginx_opt_list_input(name, &parent, size, prefix, suffix)
# Returns HTML for an optional text field with multiple values
sub nginx_opt_list_input
{
my ($name, $parent, $size, $prefix, $suffix) = @_;
return undef if (!&supported_directive($name, $parent));
my $obj = &find($name, $parent);
my $value = $obj ? join(" ", @{$obj->{'words'}}) : "";
my $def = &get_default($name);
return &ui_table_row($text{'opt_'.$name},
&ui_opt_textbox($name, $value, $size,
$text{'default'}.($def ? " ($def)" : ""), $prefix).
$suffix, $size > 40 ? 3 : 1);
}
# nginx_opt_list_parse(name, &parent, &in, [regex], [&validator])
# Updates the config with input from nginx_opt_list_input
sub nginx_opt_list_parse
{
my ($name, $parent, $in, $regexp, $vfunc) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
if ($in->{$name."_def"}) {
&save_directive($parent, $name, [ ]);
}
else {
my @v = &split_quoted_string($in->{$name});
@v || &error(&text('opt_missing', $text{'opt_'.$name}));
foreach my $v (@v) {
!$regexp || $v =~ /$regexp/ ||
&error(&text('opt_e'.$name, $v) || $name);
my $err = $vfunc && &$vfunc($v, $name);
$err && &error($err);
}
&save_directive($parent, $name, [ { 'name' => $name,
'words' => \@v } ]);
}
}
# nginx_textarea_input(name, &parent, width, height)
# Returns HTML for entering the values of multiple directives of the same type,
# in a text area
sub nginx_textarea_input
{
my ($name, $parent, $width, $height) = @_;
return undef if (!&supported_directive($name, $parent));
my @obj = &find($name, $parent);
return &ui_table_row($text{'opt_'.$name},
&ui_textarea($name,
join("\n", map { $_->{'words'}->[0] } @obj),
$height, $width), 3);
}
# nginx_textarea_parse(name, &parent, &in, [®ex], [&validator])
# Parses inputs from nginx_param_input
sub nginx_textarea_parse
{
my ($name, $parent, $in, $regexp, $vfunc) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
my @obj;
foreach my $v (split(/\r?\n/, $in->{$name})) {
!$regexp || $v =~ /$regexp/ ||
&error(&text('opt_e'.$name, $v) || $name);
my $err = $vfunc && &$vfunc($v, $name);
$err && &error($err);
push(@obj, { 'name' => $name,
'words' => [ $v ] });
}
&save_directive($parent, $name, \@obj);
}
# nginx_access_input(name1, name2, &parent)
# Returns HTML for setting allow and deny directives
sub nginx_access_input
{
my ($allow, $deny, $parent) = @_;
return undef if (!&supported_directive($allow, $parent));
my @obj = sort { $a->{'line'} <=> $b->{'line'} }
(&find($allow, $parent), &find($deny, $parent));
my $table = &ui_columns_start([ $text{'access_mode'},
$text{'access_value'} ], 100, 0,
[ "nowrap", "nowrap" ]);
my $i =0;
foreach my $o (@obj, { }, { }) {
my $v = $o->{'value'};
$v = "" if (lc($v) eq "all");
$table .= &ui_columns_row([
&ui_select($allow."_mode_".$i,
$o->{'name'},
[ [ "", " " ],
[ "allow", $text{'access_allow'} ],
[ "deny", $text{'access_deny'} ] ]),
&ui_opt_textbox($allow."_addr_".$i, $v, 30,
$text{'access_all'}, $text{'access_addr'}),
]);
$i++;
}
$table .= &ui_columns_end();
return &ui_table_row($text{'opt_'.$allow}, $table, 3);
}
# nginx_access_parse(name1, name2, &parent, &in)
# Parse inputs from nginx_access_input
sub nginx_access_parse
{
my ($allow, $deny, $parent, $in) = @_;
return undef if (!&supported_directive($allow, $parent));
$in ||= \%in;
my @obj;
my @old = sort { $a->{'line'} <=> $b->{'line'} }
(&find($allow, $parent), &find($deny, $parent));
for(my $i=0; defined(my $mode = $in->{$allow."_mode_".$i}); $i++) {
next if (!$mode);
my $addr;
if ($in->{$allow."_addr_".$i."_def"}) {
$addr = "all";
}
else {
$addr = $in->{$allow."_addr_".$i};
$addr || &error(&text('access_eaddrnone', $i+1));
&check_ipaddress($addr) ||
$addr =~ /^(\S+)\/(\d+)$/ &&
&check_ipaddress("$1") && $2 > 0 && $2 <= 32 ||
&check_ip6address($addr) ||
$addr =~ /^(\S+)\/(\d+)$/ && &check_ip6address("$1") ||
&error(&text('access_eaddr', $addr));
}
push(@obj, { 'name' => $mode,
'words' => [ $addr ] });
}
&save_directive($parent, \@old, \@obj);
}
# nginx_realm_input(name, &parent)
# Returns HTML for entering an authentication realm
sub nginx_realm_input
{
my ($name, $parent) = @_;
return undef if (!&supported_directive($name, $parent));
my $value = &find_value($name, $parent);
my $def = &get_default($name);
return &ui_table_row($text{'opt_'.$name},
&ui_radio($name."_def",
!$value ? 1 : $value eq "off" ? 2 : 0,
[ [ 1, $text{'default'}.($def ? " ($def)" : "") ],
[ 2, $text{'access_off'} ],
[ 0, $text{'access_realm'}." ".
&ui_textbox($name, $value eq "off" ? "" : $value, 40) ]
]), 3);
}
# nginx_realm_parse(name, &parent, &in)
# Updates the config with input from nginx_realm_input
sub nginx_realm_parse
{
my ($name, $parent, $in) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
if ($in->{$name."_def"} == 1) {
&save_directive($parent, $name, [ ]);
}
elsif ($in->{$name."_def"} == 2) {
&save_directive($parent, $name, [ "off" ]);
}
else {
my $v = $in->{$name};
$v eq '' && &error(&text('opt_missing', $text{'opt_'.$name}));
&save_directive($parent, $name, [ $v ]);
}
}
# nginx_passfile_input(name, &parent, server-id, path)
# Returns HTML for a password file field
sub nginx_passfile_input
{
my ($name, $parent, $id, $path) = @_;
my $value = &find_value($name, $parent);
my $edit;
if ($value =~ /^\/\S/) {
$edit = " ".
$text{'access_edit'}."";
}
return &nginx_opt_input($name, $parent, 50, $text{'access_pfile'},
&file_chooser_button($name).$edit);
}
# nginx_passfile_parse(name, &parent, &in)
# Parse input from nginx_passfile_input
sub nginx_passfile_parse
{
my ($name, $parent, $in) = @_;
$in ||= \%in;
$in->{$name."_def"} || &can_directory($in->{$name}) ||
&error(&text('access_ecannot',
"".&html_escape($in->{$name})."",
"".&html_escape($access{'root'}).""));
&nginx_opt_parse($name, $parent, $in, undef,
sub { return $_[0] !~ /^\// ? $text{'access_eabsolute'} :
-d $_[0] ? $text{'access_edir'} : undef });
}
# nginx_rewrite_input(name, &parent)
# Returns HTML for setting rewrite directives
sub nginx_rewrite_input
{
my ($name, $parent) = @_;
return undef if (!&supported_directive($name, $parent));
my @obj = &find($name, $parent);
my $table = &ui_columns_start([ $text{'rewrite_from'},
$text{'rewrite_to'},
$text{'rewrite_flag'} ], 100, 0,
[ "nowrap", "nowrap" ]);
my $i =0;
foreach my $o (@obj, { }, { }) {
$table .= &ui_columns_row([
&ui_textbox($name."_from_$i", $o->{'words'}->[0], 30),
&ui_textbox($name."_to_$i", $o->{'words'}->[1], 40),
&ui_select($name."_flag_$i", $o->{'words'}->[2],
[ map { [ $_, $text{'rewrite_'.$_} ] }
('last', 'break', 'redirect', 'permanent') ]),
]);
$i++;
}
$table .= &ui_columns_end();
return &ui_table_row($text{'opt_'.$name}, $table, 3);
}
# nginx_rewrite_parse(name1, name2, &parent, &in)
# Parse inputs from nginx_rewrite_input
sub nginx_rewrite_parse
{
my ($name, $parent, $in) = @_;
return undef if (!&supported_directive($name, $parent));
$in ||= \%in;
my @obj;
for(my $i=0; defined(my $from = $in->{$name."_from_".$i}); $i++) {
next if (!$from);
$from =~ /^\S+$/ || &error(&text('rewrite_efrom', $i+1));
my $to = $in->{$name."_to_".$i};
$to =~ /^\S+$/ || &error(&text('rewrite_eto', $i+1));
my $flag = $in->{$name."_flag_".$i};
push(@obj, { 'name' => $name,
'words' => [ $from, $to, $flag ] });
}
&save_directive($parent, $name, \@obj);
}
# list_log_formats([&server])
# Returns a list of all log format names
sub list_log_formats
{
my ($server) = @_;
my $parent = &get_config_parent();
my @rv = ( "combined" );
my $http = &find("http", $parent);
foreach my $l (&find("log_format", $http)) {
push(@rv, $l->{'words'}->[0]);
}
if ($server && $server->{'name'} eq 'server') {
foreach my $l (&find("log_format", $server)) {
push(@rv, $l->{'words'}->[0]);
}
}
return &unique(@rv);
}
# is_nginx_running()
# Returns the PID if nginx is running
sub is_nginx_running
{
my $parent = &get_config_parent();
my $pidfile = &find_value("pid", $parent);
$pidfile ||= &get_default("pid");
$pidfile ||= $config{'pid_file'};
if ($pidfile =~ /^\//) {
return &check_pid_file($pidfile);
}
else {
my ($pid) = &find_byname("nginx");
return $pid;
}
}
# stop_nginx()
# Attempt to stop nginx, return an error on failure or undef on success
sub stop_nginx
{
my $out = &backquote_logged("$config{'stop_cmd'} 2>&1 &1 &1 &1 {'value'} eq '/' } @locs;
$rootdir = $rootloc ? &find_value("root", $rootloc) : "";
}
next if ($idrootdir ne $rootdir);
return $s;
}
return undef;
}
# server_id(&server)
# Given a server, return a unique ID for it as used by the module
sub server_id
{
my ($s) = @_;
my $name = &find_value("server_name", $s);
my $rootdir = &find_value("root", $s);
if (!$rootdir) {
my @locs = &find("location", $s);
my ($rootloc) = grep { $_->{'value'} eq '/' } @locs;
if ($rootloc) {
$rootdir = &find_value("root", $rootloc);
}
$rootdir ||= "";
}
return $name.";".$rootdir;
}
# find_domain_server(&domain)
# Returns the object for a server for some domain
sub find_domain_server
{
my ($d) = @_;
my $conf = &get_config();
my $http = &find("http", $conf);
return undef if (!$http);
my @servers = &find("server", $http);
foreach my $s (@servers) {
my $obj = &find("server_name", $s);
foreach my $name (@{$obj->{'words'}}) {
if (defined($name) && (lc($name) eq lc($d->{'dom'}) ||
lc($name) eq "www.".lc($d->{'dom'}) ||
lc($name) eq "*.".lc($d->{'dom'}))) {
return $s;
}
}
}
return undef;
}
# find_location(&server, path)
# Finds the location with some path in a given server object
sub find_location
{
my ($server, $path) = @_;
foreach my $l (&find("location", $server)) {
my @w = @{$l->{'words'}};
return $l if ($w[$#w] eq $path);
}
return undef;
}
# split_ip_port(string)
# Given an ip:port pair as used in a listen directive, split them up
sub split_ip_port
{
my ($l) = @_;
if ($l =~ /^\d+$/) {
return (undef, $l);
}
elsif ($l =~ /^\[(\S+)\]:(\d+)$/) {
return ($1, $2);
}
elsif ($l =~ /^\[(\S+)\]$/) {
return ($1, 80);
}
elsif ($l =~ /^(\S+):(\d+)$/) {
return ($1, $2);
}
else {
return ($l, 80);
}
}
# server_desc(&server)
# Returns a description of a virtual host
sub server_desc
{
my ($server) = @_;
my $name = &find_value("server_name", $server);
return $name ? &text('server_desc', "".&html_escape($name)."")
: $text{'server_descnone'};
}
# location_desc(&server, &location)
# Returns a description of a location in a virtual host
sub location_desc
{
my ($server, $location) = @_;
my $name = &find_value("server_name", $server);
return $name ? &text('location_desc', "".&html_escape($name)."",
"".&html_escape($location->{'value'})."")
: &text('location_descnone',
"".&html_escape($location->{'value'})."");
}
# match_desc(string)
# Converts a location match type like ~ into a human-readable mode
sub match_desc
{
my ($m) = @_;
return $m eq "=" ? $text{'match_exact'} :
$m eq "~" ? $text{'match_case'} :
$m eq "~*" ? $text{'match_nocase'} :
$m eq "^~" ? $text{'match_noregexp'} :
$m eq "\@" ? $text{'match_named'} :
$m eq "" ? $text{'match_default'} :
"Unknown match type $m";
}
sub list_match_types
{
return ("", "=", "~", "~*", "^~", "\@");
}
# create_server_link(&server)
# Creates a link from a directory like sites-enabled to sites-available for
# a new virtual host
sub create_server_link
{
my ($server) = @_;
if ($config{'add_link'}) {
my $link = $server->{'file'};
$link =~ s/^.*\///;
$link = $config{'add_link'}."/".$link;
&symlink_logged($server->{'file'}, $link);
}
}
# delete_server_link(&server)
# Deletes the link from a directory like sites-enabled to sites-available for
# a virtual host being removed
sub delete_server_link
{
my ($server) = @_;
if ($config{'add_link'}) {
my $file = $server->{'file'};
my $short = $file;
$short =~ s/^.*\///;
opendir(LINKDIR, $config{'add_link'});
foreach my $f (readdir(LINKDIR)) {
if ($f ne "." && $f ne ".." &&
(&resolve_links($config{'add_link'}."/".$f) eq $file ||
$short eq $f)) {
&unlink_logged($config{'add_link'}."/".$f);
}
}
closedir(LINKDIR);
}
}
# delete_server_file_if_empty(&server)
# If the file for a server is empty, delete it
sub delete_server_file_if_empty
{
my ($server) = @_;
my $lref = &read_file_lines($server->{'file'}, 1);
my $count = 0;
foreach my $l (@$lref) {
$count++ if ($l =~ /\S/);
}
if (!$count) {
&unlink_logged($server->{'file'});
}
}
# valid_cert_file(filename)
# Returns an error message if a cert file is invalid, or undef if OK
sub valid_cert_file
{
my ($file) = @_;
-r $file && !-d $file || return $text{'ssl_ecertfile'};
my $data = &read_file_contents($file);
my @lines = grep { /\S/ } split(/\r?\n/, $data);
my $begin = "-----BEGIN CERTIFICATE-----";
my $end = "-----END CERTIFICATE-----";
$data =~ /$begin/ ||
return &text('ssl_ecertbegin', "-----BEGIN CERTIFICATE-----");
$data =~ /$end/ ||
return &text('ssl_ecertend', "-----END CERTIFICATE-----");
for(my $i=0; $i<@lines; $i++) {
$lines[$i] =~ /^-----(BEGIN|END)/ ||
$lines[$i] =~ /^[A-Za-z0-9\+\/=]+$/ ||
return &text('ssl_ecertline', $i+1);
}
@lines > 4 || return &text('ssl_ecertlines', scalar(@lines));
return undef;
}
# valid_key_file(filename)
# Returns an error message if a key file is invalid, or undef if OK
sub valid_key_file
{
my ($file) = @_;
-r $file && !-d $file || return $text{'ssl_ekeyfile'};
my $data = &read_file_contents($file);
my @lines = grep { /\S/ } split(/\r?\n/, $data);
my $begin = "-----BEGIN (RSA )?PRIVATE KEY-----";
my $end = "-----END (RSA )?PRIVATE KEY-----";
$data =~ /$begin/ ||
return &text('ssl_ekeybegin', "-----BEGIN PRIVATE KEY-----");
$data =~ /$end/ ||
return &text('ssl_ekeyend', "-----END PRIVATE KEY-----");
for(my $i=0; $i<@lines; $i++) {
$lines[$i] =~ /^-----(BEGIN|END)/ ||
$lines[$i] =~ /^[A-Za-z0-9\+\/=]+$/ ||
return &text('ssl_ekeyline', $i+1);
}
@lines > 4 || return &text('ssl_ekeylines', scalar(@lines));
return undef;
}
# can_edit_server(&server)
# Returns 1 if some server can be managed
sub can_edit_server
{
my ($server) = @_;
return 1 if (!$access{'vhosts'});
my $name = &find_value("server_name", $server);
return 0 if (!$name);
return &indexoflc($name, split(/\s+/, $access{'vhosts'})) >= 0;
}
# can_directory(dir)
# Check if some directory is under one of the allowed roots
sub can_directory
{
my ($dir) = @_;
foreach my $root (split(/\s+/, $access{'root'})) {
return 1 if (&is_under_directory($root, $dir));
}
return 0;
}
# switch_write_user(mode)
# If mode is 1, switch to another user for writing password files.
# If 0, switch back to root.
sub switch_write_user
{
my ($mode) = @_;
return if ($access{'user'} eq 'root');
if ($mode) {
my @uinfo = getpwnam($access{'user'});
@uinfo || &error("Write user $access{'user'} does not exist!");
$) = $uinfo[3]." ".join(" ", $uinfo[2], &other_groups($uinfo[0]));
$> = $uinfo[2];
}
else {
$) = 0;
$> = 0;
}
}
# recursive_change_directives(&parent, old-value, new-value, [suffix-too],
# [prefix-too], [infix-too], [&skip-named])
# Change all directives who have a value that is the old value to the new one
sub recursive_change_directives
{
my ($parent, $oldv, $newv, $suffix, $prefix, $infix, $skip) = @_;
return if (!$oldv);
foreach my $dir (@{$parent->{'members'}}) {
if (!$skip || &indexof($dir->{'name'}, @$skip) < 0) {
my $changed = 0;
foreach my $w (@{$dir->{'words'}}) {
my $ow = $w;
if ($infix && $w =~ /\Q$oldv\E/) {
$w =~ s/\Q$oldv\E/$newv/g;
$changed++;
}
elsif ($suffix && $w =~ /\Q$oldv\E$/) {
$w =~ s/\Q$oldv\E$/$newv/;
$changed++;
}
elsif ($prefix && $w =~ /^\Q$oldv\E/) {
$w =~ s/^\Q$oldv\E/$newv/;
$changed++;
}
elsif ($w eq $oldv) {
$w = $newv;
$changed++;
}
if ($ow ne $w) {
print STDERR "changed $ow to $w\n";
}
}
if ($changed) {
&save_directive($parent, [ $dir ], [ $dir ]);
}
}
if ($dir->{'type'}) {
&recursive_change_directives($dir, $oldv, $newv,
$suffix, $prefix);
}
}
}
# list_available_fcgid_php_versions(&domain)
# List PHP versions that can be used in FCGId mode
sub list_available_fcgid_php_versions
{
my @vers = &virtual_server::list_available_php_versions(undef, "fcgid");
my @rv;
foreach my $v (@vers) {
if ($v->[1]) {
&clean_environment();
my $out = &backquote_command("$v->[1] -h 2>&1 [0] <=> $a->[0] } &list_available_fcgid_php_versions($d);
@vers || return ( );
if ($d->{'nginx_php_version'}) {
# Try to get the version the domain is using
my ($tver) = grep { $_->[0] eq $d->{'nginx_php_version'} } @vers;
if ($tver) {
return @$tver;
}
}
# Fall back to the first one available
my $cmd = $vers[0]->[1];
$cmd || return ( );
return @{$vers[0]};
}
# create_loop_script()
# Returns the path to a script that runs PHP in a loop
sub create_loop_script
{
foreach my $looper ("/usr/bin/php-loop.pl", "/etc/php-loop.pl") {
if (©_source_dest("$module_root_directory/php-loop.pl", $looper)) {
return $looper;
}
}
&error("Could not copy php-loop.pl to anywhere!");
}
# get_php_fcgi_server_command(&domain, port|file)
# Returns the PHP CGI command and a hash ref of environment variables
sub get_php_fcgi_server_command
{
my ($d, $port) = @_;
my ($ver, $basecmd) = &get_domain_php_version($d);
$basecmd || return ( );
my $cmd = $basecmd;
my $log = "$d->{'home'}/logs/php.log";
my $piddir = "/var/php-nginx";
if (!-d $piddir) {
&make_dir($piddir, 0777);
}
my $pidfile = "$piddir/$d->{'id'}.php.pid";
if ($port =~ /^\d+$/) {
$cmd .= " -b 127.0.0.1:$port";
}
else {
$cmd .= " -b $port";
}
my %envs_to_set = ( 'PHPRC', $d->{'home'}."/etc/php".$ver );
if ($d->{'nginx_php_children'} && $d->{'nginx_php_children'} > 1) {
$envs_to_set{'PHP_FCGI_CHILDREN'} = $d->{'nginx_php_children'};
}
$cmd = &create_loop_script()." ".$cmd;
return ($cmd, \%envs_to_set, $log, $pidfile, $basecmd);
}
# setup_php_fcgi_server(&domain)
# Starts up a PHP process running as the domain user, and enables it at boot.
# Returns an OK flag and the port number selected to listen on.
sub setup_php_fcgi_server
{
my ($d) = @_;
my $port;
if (!$config{'php_socket'}) {
# Find ports used by domains
my %used;
foreach my $od (&virtual_server::list_domains()) {
if ($od->{'id'} ne $d->{'id'} && $od->{'nginx_php_port'}) {
$used{$od->{'nginx_php_port'}}++;
}
}
# Find a free port
$port = 9100;
my $s;
socket($s, PF_INET, SOCK_STREAM, getprotobyname('tcp')) ||
return (0, "Socket failed : $!");
setsockopt($s, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
while(1) {
last if (!$used{$port} &&
bind($s, sockaddr_in($port, INADDR_ANY)));
$port++;
}
close($s);
}
else {
# Use socket file. First work out directory for it
my $socketdir = "/var/php-nginx";
if (!-d $socketdir) {
&make_dir($socketdir, 0777);
}
my $domdir = "$socketdir/$d->{'id'}.sock";
if (!-d $domdir) {
&make_dir($domdir, 0770);
}
my $user = &get_nginx_user();
&set_ownership_permissions($user, $d->{'gid'}, undef, $domdir);
$port = "$domdir/socket";
}
# Get the command
my ($cmd, $envs_to_set, $log, $pidfile, $basecmd) =
&get_php_fcgi_server_command($d, $port);
$cmd || return (0, $text{'fcgid_ecmd'});
# Check that the PHP command supports the -b flag
&clean_environment();
my $out = &backquote_command("$basecmd -h 2>&1 $basecmd",
"$out"));
}
if ($out !~ /\s-b\s/) {
return (0, &text('fcgid_ecmdb', "$basecmd"));
}
# Create init script
&foreign_require("init");
my $old_init_mode = $init::init_mode;
if ($init::init_mode eq "upstart") {
$init::init_mode = "init";
}
my $name = &init_script_name($d);
my $envs = join(" ", map { $_."=".$envs_to_set->{$_} } keys %$envs_to_set);
my %cmds_abs = (
'echo', &has_command('echo'),
'cat', &has_command('cat'),
'chmod', &has_command('chmod'),
'kill', &has_command('kill'),
'sleep', &has_command('sleep'),
);
if (defined(&init::enable_at_boot_as_user)) {
# Init system can run commands as the user
&init::enable_at_boot_as_user($name,
"Starts Nginx PHP FastCGI server for $d->{'dom'} (Virtualmin)",
"$envs $cmd >>$log 2>&1 $pidfile",
"$cmds_abs{'kill'} `$cmds_abs{'cat'} $pidfile`",
undef,
{ 'fork' => 1,
'pidfile' => $pidfile },
$d->{'user'},
);
}
else {
# Older Webmin requires use of command_as_user
&init::enable_at_boot($name,
"Starts Nginx PHP FastCGI server for $d->{'dom'} (Virtualmin)",
&command_as_user($d->{'user'}, 0,
"$envs $cmd >>$log 2>&1 $pidfile && $cmds_abs{'chmod'} +r $pidfile",
&command_as_user($d->{'user'}, 0,
"$cmds_abs{'kill'} `$cmds_abs{'cat'} $pidfile`")." ; $cmds_abs{'sleep'} 1",
undef,
{ 'fork' => 1,
'pidfile' => $pidfile },
);
}
$init::init_mode = $old_init_mode;
# Launch it, and save the PID
&init::start_action($name);
return (1, $port);
}
# delete_php_fcgi_server(&domain)
# Shut down the fcgid server process, and delete it from starting at boot
sub delete_php_fcgi_server
{
my ($d) = @_;
# Stop the server
&foreign_require("init");
my $name = &init_script_name($d);
&init::stop_action($name);
# Delete init script
my $old_init_mode = $init::init_mode;
if ($init::init_mode eq "upstart") {
$init::init_mode = "init";
}
&init::disable_at_boot($name);
&init::delete_at_boot($name);
$init::init_mode = $old_init_mode;
# Previously we created init scripts under system
if ($init::init_mode eq "systemd") {
my $old_init_mode = $init::init_mode;
$init::init_mode = "init";
&init::disable_at_boot($name);
&init::delete_at_boot($name);
$init::init_mode = $old_init_mode;
}
# Delete socket file, if any
if ($d->{'nginx_php_port'} && $d->{'nginx_php_port'} =~ /^(\/\S+)\/socket$/) {
my $domdir = $1;
&unlink_file($d->{'nginx_php_port'});
&unlink_file($domdir);
}
}
# init_script_name(&domain)
# Returns the name of the init script for the FCGId server
sub init_script_name
{
my ($d) = @_;
my $name = "php-fcgi-$d->{'dom'}";
$name =~ s/\./-/g;
return $name;
}
# find_php_fcgi_server(&domain)
# Returns the full path to the PHP command used by this domain's fcgi server
sub find_php_fcgi_server
{
my ($d) = @_;
&foreign_require("init");
my $old_init_mode = $init::init_mode;
if ($init::init_mode eq "upstart" ||
$init::init_mode eq "systemd") {
$init::init_mode = "init";
}
# Find the script that runs php
my $name = "php-fcgi-$d->{'dom'}";
my $oldname = $name;
$name =~ s/\./-/g;
my $script;
foreach my $n ($name, $oldname) {
my $fn;
if ($init::init_mode eq "init") {
$fn = &init::action_filename($n);
}
elsif ($init::init_mode eq "rc") {
my @rcs = &init::list_rc_scripts();
my ($rc) = grep { $_->{'name'} eq $n } @rcs;
if ($rc) {
$fn = $rc->{'file'};
}
}
if ($fn && -r $fn) {
$script = $fn;
last;
}
}
$init::init_mode = $old_init_mode;
return undef if (!$script);
# Extract the PHP command from it
my $lref = &read_file_lines($script, 1);
my $cmd;
foreach my $l (@$lref) {
if ($l =~ /su\s+(\S+)\s+-c\s+(.*)/ &&
$1 eq $d->{'user'}) {
# Possible command line - need to unquotemeta
my $sucmd = $2;
$sucmd = eval "\"$sucmd\"";
if ($sucmd =~ /php-loop.pl\s+(\S+)/) {
$cmd = $1;
last;
}
}
}
return $cmd;
}
# list_fastcgi_params(&server)
# Returns a list of param names and values needed for fastCGI
sub list_fastcgi_params
{
my ($server) = @_;
my $root = &find_value("root", $server);
$root ||= '$realpath_root';
my @rv = (
[ 'GATEWAY_INTERFACE', 'CGI/1.1' ],
[ 'SERVER_SOFTWARE', 'nginx' ],
[ 'QUERY_STRING', '$query_string' ],
[ 'REQUEST_METHOD', '$request_method' ],
[ 'CONTENT_TYPE', '$content_type' ],
[ 'CONTENT_LENGTH', '$content_length' ],
[ 'SCRIPT_FILENAME', $root.'$fastcgi_script_name' ],
[ 'SCRIPT_NAME', '$fastcgi_script_name' ],
[ 'REQUEST_URI', '$request_uri' ],
[ 'DOCUMENT_URI', '$document_uri' ],
[ 'DOCUMENT_ROOT', $root ],
[ 'SERVER_PROTOCOL', '$server_protocol' ],
[ 'REMOTE_ADDR', '$remote_addr' ],
[ 'REMOTE_PORT', '$remote_port' ],
[ 'SERVER_ADDR', '$server_addr' ],
[ 'SERVER_PORT', '$server_port' ],
[ 'SERVER_NAME', '$server_name' ],
[ 'PATH_INFO', '$fastcgi_path_info' ],
);
my $ver = &get_nginx_version();
if ($ver =~ /^(\d+)\./ && $1 >= 2 ||
$ver =~ /^1\.(\d+)\./ && $1 >= 2 ||
$ver =~ /^1\.1\.(\d+)/ && $1 >= 11) {
# Only in Nginx 1.1.11+
push(@rv, [ 'HTTPS', '$https' ]);
}
return @rv;
}
# find_before_location(&parent, path)
# Finds the first location with a path shorter than the one given
sub find_before_location
{
my ($parent, $path) = @_;
my @locs = &find("location", $parent);
foreach my $l (@locs) {
if (length($l->{'words'}->[0]) <= length($path)) {
return $l;
}
}
return undef;
}
# setup_fcgiwrap_server(&domain)
# Starts up a fcgiwrap process running as the domain user, and enables it
# at boot time. Returns an OK flag and the port number selected to listen on.
sub setup_fcgiwrap_server
{
my ($d) = @_;
# Work out socket file for fcgiwrap
my $socketdir = "/var/fcgiwrap";
if (!-d $socketdir) {
&make_dir($socketdir, 0777);
}
my $domdir = "$socketdir/$d->{'id'}.sock";
if (!-d $domdir) {
&make_dir($domdir, 0770);
}
my $user = &get_nginx_user();
&set_ownership_permissions($user, $d->{'gid'}, undef, $domdir);
my $port = "$domdir/socket";
# Get the command
my ($cmd, $log, $pidfile) = &get_fcgiwrap_server_command($d, $port);
$cmd || return (0, $text{'fcgid_ecmd'});
# Create init script
&foreign_require("init");
my $old_init_mode = $init::init_mode;
if ($init::init_mode eq "upstart") {
$init::init_mode = "init";
}
my $name = &init_script_fcgiwrap_name($d);
my %cmds_abs = (
'echo', &has_command('echo'),
'cat', &has_command('cat'),
'chmod', &has_command('chmod'),
'kill', &has_command('kill'),
'sleep', &has_command('sleep'),
'fuser', &has_command('fuser'),
'rm', &has_command('rm'),
);
if (defined(&init::enable_at_boot_as_user)) {
# Init system can run commands as the user
&init::enable_at_boot_as_user($name,
"Nginx FCGIwrap server for $d->{'dom'} (Virtualmin)",
"$cmds_abs{'rm'} -f $port ; $cmd >>$log 2>&1 $pidfile && sleep 2 && $cmds_abs{'chmod'} 777 $port",
"$cmds_abs{'kill'} `$cmds_abs{'cat'} $pidfile` ; ".
"$cmds_abs{'sleep'} 1 ; ".
"$cmds_abs{'rm'} -f $port",
undef,
{ 'fork' => 1,
'pidfile' => $pidfile },
$d->{'user'},
);
}
else {
# Older Webmin requires use of command_as_user
&init::enable_at_boot($name,
"Nginx FCGIwrap server for $d->{'dom'} (Virtualmin)",
&command_as_user($d->{'user'}, 0,
"$cmd >>$log 2>&1 $pidfile && $cmds_abs{'chmod'} +r $pidfile && sleep 2 && $cmds_abs{'chmod'} 777 $port",
&command_as_user($d->{'user'}, 0,
"$cmds_abs{'kill'} `$cmds_abs{'cat'} $pidfile`").
" ; $cmds_abs{'sleep'} 1".
($cmds_abs{'fuser'} ? " ; $cmds_abs{'fuser'} $port | xargs kill"
: "").
" ; $cmds_abs{'rm'} -f $port",
undef,
{ 'fork' => 1,
'pidfile' => $pidfile },
);
}
$init::init_mode = $old_init_mode;
# Launch it, and save the PID
&init::start_action($name);
return (1, $port);
}
# delete_fcgiwrap_server(&domain)
# Shut down the fcgiwrap process, and delete it from starting at boot
sub delete_fcgiwrap_server
{
my ($d) = @_;
# Stop the server
&foreign_require("init");
my $name = &init_script_fcgiwrap_name($d);
&init::stop_action($name);
# Delete init script
my $old_init_mode = $init::init_mode;
if ($init::init_mode eq "upstart") {
$init::init_mode = "init";
}
&init::disable_at_boot($name);
&init::delete_at_boot($name);
$init::init_mode = $old_init_mode;
# Delete socket file, if any
if ($d->{'nginx_fcgiwrap_port'} &&
$d->{'nginx_fcgiwrap_port'} =~ /^(\/\S+)\/socket$/) {
my $domdir = $1;
&unlink_file($d->{'nginx_fcgiwrap_port'});
&unlink_file($domdir);
}
}
# get_fcgiwrap_server_command(&domain, port)
# Returns a command to run the fcgiwrap server, log file and PID file
sub get_fcgiwrap_server_command
{
my ($d, $port) = @_;
my $cmd = &has_command("fcgiwrap");
$cmd .= " -s unix:".$port;
my $log = "$d->{'home'}/logs/fcgiwrap.log";
my $piddir = "/var/php-nginx";
if (!-d $piddir) {
&make_dir($piddir, 0777);
}
my $pidfile = "$piddir/$d->{'id'}.fcgiwrap.pid";
return ($cmd, $log, $pidfile);
}
# init_script_fcgiwrap_name(&domain)
# Returns the name of the init script for the FCGId server
sub init_script_fcgiwrap_name
{
my ($d) = @_;
my $name = "fcgiwrap-$d->{'dom'}";
$name =~ s/\./-/g;
return $name;
}
# url_to_upstream(url)
# Converts a URL like http://www.foo.com/ to an upstream host:port spec
sub url_to_upstream
{
my ($url) = @_;
my ($host, $port, $uds) = &parse_backend($url);
return $port if ($uds);
$port ||= 80;
return $host.":".$port;
}
# upstream_to_url(host:port)
# Converts a host:port spec to a URL
sub upstream_to_url
{
my ($hp) = @_;
my ($host, $port) = split(/:/, $hp);
return "http://".$host.($port == 80 ? "" : ":".$port);
}
# validate_balancer_urls(url, ...)
# Checks a bunch of URLs for syntax and resolvability.
# If socket is used, it is not checked for resolvability.
sub validate_balancer_urls
{
foreach my $u (@_) {
my ($host, $port, $uds) = &parse_backend($u);
# Check for valid URL
if (!$uds) {
return &text('redirect_eurl', $u) if (!$host);
&to_ipaddress($host) || &to_ip6address($host) ||
return &text('redirect_eurlhost', $host);
}
# Socket is not checked for resolvability and already validated
}
return undef;
}
# split_ssl_certs(data)
# Returns an array of all SSL certs in some file
sub split_ssl_certs
{
my ($data) = @_;
my @rv;
my $idx = -1;
foreach my $l (split(/\r?\n/, $data)) {
if ($l =~ /^\-+BEGIN/) {
$idx++;
push(@rv, $l."\n");
}
elsif ($idx >= 0 && $l =~ /\S/) {
$rv[$idx] .= $l."\n";
}
}
return @rv;
}
# recursive_clear_lines(&directive, ...)
# Remove any file and line fields from directives
sub recursive_clear_lines
{
foreach my $e (@_) {
delete($e->{'file'});
delete($e->{'line'});
delete($e->{'eline'});
if ($e->{'type'}) {
&recursive_clear_lines(@{$e->{'members'}});
}
}
}
# parse_backend(url|sock)
# Parses a URL into host and port or unix domain socket and
# sets a flag if it is socket
sub parse_backend
{
my ($url) = @_;
if ($url =~ /^http:\/\/(unix:\/\S+)/) {
return (undef, $1, 1);
}
else {
my ($host, $port) = &parse_http_url($url);
return ($host, $port, 0);
}
return (undef, undef, undef);
}
1;