#!/usr/bin/perl -U
# virtual hosting script v0.5.x (c) Denis Kaganovich AKA mahatma
# redirect & compress by gzip on-the-fly multiple vhosts witheout real
# virtual hosts support on site. very simple!

### config begin
my $root=$ENV{DOCUMENT_ROOT}||'.';
my $mode=1; # 0-single, 1-www.doe.com-"doe/", 2-"www.doe.com/"
my $index=['index.htm','index.html'];
my $linkindex='index.cgi'; # indexfile link for $linktype=1
my $codepage='koi8-r'; # codepage ID to send with HTTP header with "text/*" types.
my $enable_gzip=1;
my $vbase="$root/"; # base path to real vhosts repository
my $logs="$root/log/"; # path to logs
my $loglevel=1; # 0-4 (0-off)
my $linktype=0; # 0-symlink; 1-dir; 2,3-internal (don't touch); for 1 must be absolute $vbase
my $cache=1; # cache .gz?
my $zbase="$root/cache/"; # path to gzip cache
my $gzip='/usr/bin/gzip'; # gzip
my $mkdir_mode=0770; # for $linktype=1
my $ExecCGI=1; # 0-no; 1-yes/auto; 2-force gz/nocached; 3-force gz/cache (bad, experemental)
my $suexec=0; # exec CGI under file UID/GID? (untested)
### config end

my %mime=(
'.html'=>'text/html',
'.htm'=>'text/html',
'.txt'=>'text/plain',
'.cgi'=>'text/plain',
'.js'=>'text/javascript',
#'js'=>'application/x-javascript',
'.gif'=>'image/gif',
'.jpg'=>'image/jpeg',
'.gz'=>'application/x-gzip'
);

my %mime_gz=( # -1
'Lynx'=>{
	'text/html'=>2,
	'text/plain'=>2,
	'*'=>1
}, # for Lynx gzip only text/html & text/plain
'*'=>{
	'image/jpeg'=>1,
	'*'=>2
} # for others compress all exclude jpeg
);

my %cgi=(
'.cgi'=>1,
'.pl'=>1,
'.php3'=>1,
'.php'=>1
);


$enable_gzip=index($ENV{HTTP_ACCEPT_ENCODING},'gzip',0)>=0?$enable_gzip:0;
my $hthead='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">';
my $iam=$ENV{SCRIPT_FILENAME};
my $i;
my $ndx='';
my $txt='';
my $cnt=0;
my $p=$mode==0?'':lc($ENV{HTTP_HOST});
if($mode==1){
	$p=substr($p,4) if(index($p,'www.')==0);
	$p=substr($p,0,index($p,'.'));
}elsif($mode==0){chop($vbase) if(substr($vbase,-1) eq '/')};
my $rs=$ENV{REDIRECT_STATUS}+0;
my $f=$ENV{REQUEST_URI};
my $meth=$ENV{REQUEST_METHOD}||"?";
my $log=$loglevel>0?localtime(time)." - $ENV{REMOTE_ADDR}:$ENV{REMOTE_PORT}".($loglevel>1?log2($ENV{HTTP_X_FORWARDED_FOR}):'')." $meth $ENV{HTTP_HOST}$f".($loglevel>2?' '.($ENV{HTTP_REFERER}||'?'):''):'';
my @fs;
$i=index($f,'?'); $f=substr($f,0,$i) if($i>0);
my $ff="$vbase$p$f";
my $t;
if($rs==404&&substr($f,-1) ne '/'&&-d $ff){$ff.='/';$f.='/'}
if(substr($f,-1) eq '/'){		
	for ($i=0;$i<scalar(@$index) && !(@fs=stat("$ff".($ndx=@$index[$i]))); $i++){};
	if(@fs){$f.=$ndx; $ff.=$ndx}
	else{
		$t='.html';
		chop($i) if(length($i=$f)>1);
		$txt.="$hthead\n<html><head><title>Index of $i</title></head><body><table width=100%><tr><th>Name</th><th>Size</th><th>Date</th><th>Description</th></tr>";
		opendir DH,$ff or err(404,"path not found");
		my @dir=readdir(DH);
		@fs=stat($ff);
		my @stat;
		for $i (@dir){
			@stat=stat("$ff$i");
			@fs[9]=@stat[9] if(@fs[9]<@stat[9]);
			$txt.="\n<tr><td><a href=\"$i\">$i</a></td><td>@stat[7]</td><td>".localtime(@stat[9])."</td><td>&nbsp;</td></tr>"
		}
		closedir DH;
		$txt.='</table></body></html>';
		$ff='';
	}

}
$t=$t||lc(substr($f,$i=rindex($f,'.')));
my $zz=0;
my $loc=$f;
@fs=(@fs[9])?@fs:stat($ff);
err(404,'not found') if(!@fs);
my $a=$ENV{HTTP_USER_AGENT};
$log.=" \"$a\"" if($loglevel>3);
$a=substr($a,0,index($a,'/'));
$log.=" $a" if($loglevel>0 && $loglevel<4);
my $m;
my $h='';
my $hdr=sub {$h.="$_[0]$_[1]\n";};

if($ExecCGI>0 && $ff ne '' && $cgi{$t}==1 && -x $ff){
    $hdr=sub {
	my $t=shift;
	my $x=shift;
	my $i=index(lc($h),lc($t));
	if($i<0){$h.="$t$x\n" if($x); return;}
	$i+=length($t);
	return substr($h,$i,index($h,"\n",$i)-$i,$x) if($x);
	return substr($h,$i,index($h,"\n",$i)-$i);
    };
	$log.=" - &$ff" if($loglevel>0);
	$t='.txt';
	my $q='';
	if($suexec){
	    $)="@fs[5] @fs[5]"; $(=@fs[5]; $<=$>=@fs[4];
	    err(500,'cgi security error') if($) ne "@fs[5] @fs[5]" or $(!=@fs[5] or $<!=@fs[4] or $>!=@fs[4]);	  
	}
	open FH,"$ff|" or err(500,'cgi error'); binmode FH;
	$txt.=<FH> while(!eof(FH));
	close(FH);
	if($enable_gzip==0||$ExecCGI==1){
		pr($txt);
		lexit(0);
	}
	$h=substr($txt,0,index($txt,"\n\n")+2,'');
	chomp($h);
	$m=&$hdr('Content-type: ')||'*/*';
	if(($i=index($i,";"))<0){
		$i=$m;
		$m.="; codepage=$codepage" if($codepage ne '' && index($m,'text/')>=0);
	}else{$i=substr($m,0,$i);};
	my $e=&$hdr('Content-encoding: ');
	if($e eq 'gzip'){
		pr($h,"\n",$txt);
		lexit(0);
	}
	$enable_gzip=0 if($ExecCGI==1 && $cache==1);
	$cache=0 if($ExecCGI<3);
}else{
	if($enable_gzip==1 && $t eq '.gz'){
	my $j=rindex($f,'.',$i-1);
	$t=lc(substr($f,$j,$i-$j));
	$zz=1;
	}
	$i=$m=$mime{$t}||'*/*';
	$m.="; codepage=$codepage" if($codepage ne '' && index($m,'text/')>=0);
};
&$hdr('Content-Type: ',$m);

if($enable_gzip==1){
my $fz="$zbase$p$f.gz";
my $z=$mime_gz{$a}||$mime_gz{'*'}||{'*'=>2};
$z=($z->{$i}||$z->{'*'}||2)-1;
if($z==1||$zz==1){
	if($zz==0){
	$loc.='.gz';
	if($cache==0){$ff=$txt eq ''?"$gzip -cfn9 $ff |":"|$gzip -cfn9"}
	else{
		my @fzs=stat($fz);
		if((@fzs[9]||-1)<@fs[9]){
			mklink($fz,4,length($zbase));
			if($txt eq ''){`$gzip -cfn9 $ff >$fz`}
			else{
				open FH, "|$gzip -cfn9 >$fz";
				print FH $txt;
				close(FH);
				$txt=''
			}
			@fzs=stat($fz)
		}
		@fs[7]=@fzs[7];
		$ff=$fz
	}
	}
	&$hdr('Content-Encoding: ','gzip') if($t ne '.tar');
}
}

&$hdr('Content-Location: ',$loc);
&$hdr('Last-Modified: ',localtime(@fs[9]));
$log.=log2($ff) if($loglevel>0);
if($txt ne '' && $ff eq ''){&$hdr('Content-Length: ',length($txt));pr($h,"\n",($meth ne 'HEAD')?$txt:'')}
else{
	mklink("$root$f$ndx",$linktype,length($root)) if($rs==404||$rs==403);
	&$hdr('Content-Length: '.@fs[7]) if(index($ff,'|')<0);
	pr($h,"\n");
	if($meth ne 'HEAD'){
		open FH,$ff or err(403,'access denied'); binmode FH;
		if($cache==0 && $txt ne ''){print FH $txt;$cnt='?'};
		pr(<FH>);
		close(FH);
	};
}

lexit(0);

#################################################

sub log2{
my $l='';
while(my $i=shift){
$i="\"$i\"" if(index($i,' ')>=0);
$i="-" if($i eq '');
$l.=" $i";
}
return $l;
}

sub pr{
while(my $i=shift){$cnt+=length($i);print $i}
};

sub mklink{
my $r=shift||return 1;
my $lnk=shift; # 0-symlink; 1-dir; 2-dir w/o last; 3-experimental, not work now
my $i0=shift||0;
my ($i,$i1)=(0,0);
my $l=length($r);
my $rr;
while($i0<=$l){
	$i=index($r,'/',$i0);
	$i1=$i<0?$l:$i;
	$rr=substr($r,0,$i1);
	if($lnk==3||($lnk==0 && $i>=0)){symlink('.',$rr)}
	elsif($lnk==1 && $i<0 && substr($r,-1) eq '/'){symlink($iam,"$r$linkindex")}
	elsif($lnk==0||($lnk==1 && $i<0)){symlink($iam,$rr)}
	elsif($lnk>0 && $i>=0){mkdir($rr,$mkdir_mode)}
	$i0=$i1+1;
}
}

sub err{
my $e=shift;
my $t=shift;
pr(qq(Content-Type: text/html
Pragma: no-cache
Content-Location: /error/$e.html

$hthead
<html><head><title>$e - $t</title></head><body>
<center><b>Error $e</b><br>$ENV{REQUEST_URI}<br>$t</center>
</body></html>));
lexit($e);
}

sub lexit{
my $e=shift;
if($loglevel>0){
open FL, ">>$logs$p.log" or die "log error";
if($loglevel>1){
    my ($t1,$t2,$t3,$t4)=times;
    $log.=" $cnt $t1/$t2/$t3/$t4";
}
print FL "$log - $e\n";
close FL;
}
exit($e);
}


__END__
=head1 NAME

vhscript-0.5.3.pl (AKA index.cgi) - Virtual Hosting Script (+accelerator/gzip).

=head1 DESCRIPTION

Allow alternative ways to:
1) virtual hosting;
2) transparently compress (accelerate) traffic by gzip.

=head1 README

Virtual Hosting Script	v0.5.3		(c) Denis Kaganovich AKA mahatma

There are simple script, that allow to alternative ways to:
1) virtual hosting;
2) transparently compress (accelerate) traffic by gzip.

(c)opyleft. Free. You MUST change code to tune.

WARNING: slotly tested, I have not security ideas. May be there are simple
large gap to your system, may be not. Try if sure. I am use it. Please,
don't write me nothing about changes, just do it self.

Installation:

Select ways to host. There are 3 modes ($mode):
0. Single virtual host.
1. Default: every vhost last level name lowercase witheout "www".
Examples: "www.doe.com" - "doe", "doe.com" - "doe".
2. Full host name lowercase.

Recommended name of script are "index.cgi".

Change "$enable_gzip" to "0" to turn off compression (default - ON if supported
by client).

Move all your [compressible] files and subdirectories into preferred directory.

Change ".htaccess" file something like this:
---
Options ExecCGI FollowSymLinks
ErrorDocument 403 /index.cgi
ErrorDocument 404 /index.cgi
AddHandler cgi-script .html .cgi .txt .jpg .htm .gif .js .bbs .rar .zip .tgz .exe .doc .pdf
---

Create writable cache (default - ".gz" ) directory if gzip & cache enabled.

a) If your hoster supported for "ErrorDocument" in .htaccess - just try to access
your files. First request will be "404", but file will sended. Every next
request will be clean.


b) If your hoster are not support "ErrorDocument" - create:

dirtype=0 - symlinks in root: for every your directory, linked to ".",
for every file - linked to script. 

dirtype=1 - full directory structure and symlinks for files, linked to script.

Make "AddHandler cgi-script ..." to all file types and  script type (now
".cgi").

Edit config section.

Tested with Perl 5.8.0 & Apache/1.3.28. Some with older Perl/Apache.
No perl modules usage.

=head1 PREREQUISITES

Perl 5.6.0 (last tested with 5.8.0, but IMHO stay compatible).

=head1 COREQUISITES

Perl 5, no modules

=pod OSNAMES

All

=pod SCRIPT CATEGORIES

Web, CGI

=cut
