#!C:\Perl\bin\perl.exe -w # # $Id: fs2htaccess.pl 138 2005-03-31 09:26:35Z $ # # fs2htaccess Version 0.1.1 - this perl script transports windows file # and directory permissions to apache per-directory configs. # # Copyright (C) 2005 Markus Siebeneicher # # This program is free software; you can redistribute # it and/or modify it under the terms of the GNU General # Public License as published by the Free Software Foundation; # either version 2 of the License, or (at your option) any # later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. # ################################################################################# =pod =pod SCRIPT CATEGORIES Win32 =pod OSNAMES Win32 =pod README fs2htaccess transports windows file and directory permissions to apache per-directory configs. =head1 NAME B - this perl script transports windows file and directory permissions to apache per-directory config. =head1 SYNOPSIS # simple process one directory and write .htaccess config file perl c:\programms\fs2htaccess.pl s:\path # recurse all sub-directories, set HIDDEN attribute to config file and process current working directory cd d:\data perl c:\programms\fs2htaccess.pl -rs . # ignore all files beginning with a dot and write config file named: httpd.conf perl c:\programms\fs2htaccess.pl -c httpd.conf -i ^\. d: # print help page perl c:\programms\fs2htaccess.pl -h # ... perl fs2htaccess.pl -vsr -i ^\. s:\ # example of generated apache per-directory config require user Administrator admin Siebeneicher require group Domänen-Admins require user Administrator admin Siebeneicher require group Domänen-Admins require user Administrator admin Siebeneicher require group Domänen-Admins require user Administrator admin Siebeneicher require group Domänen-Admins =head1 PREREQUISITES =begin html This script depends on the Win32::Perms module which is maintained by the Roth Consultings. =end html =head1 DESCRIPTION fs2htaccess.pl is a simple perl script for win32 systems and was developed under a w2k system using ActivePerl 5.8.6. fs2htaccess.pl scan's a given directory for files. for each file it looks which windows system account have READ(FILE_READ_DATA STANDARD_RIGHTS_ALL READ_CONTROL) permission, than all of these accounts will be transported -- user or group account -- to the apache per-directory config. if a file have permission restrictions the entry in your config could look like: require user dude require group personal admins if there are only permission restrictions to a user or group account, only that user or group will be transported. if the config file is empty after processing a directory, that comes if all folks can read the folder and files, the config file will be deleted. if there was also an previous defined config in the folder all included apache directives, without "require ..." will be kept in it. if the argument B<-r> was given all sub-directories will be process too. under different locale windows has pre-defined a group called 'Everyone' which gives all users and group same permission. see argument B<-m>. =head2 PRACTICAL NOTES =over =item - you should be admin or a user who have read/write permission to the folder and config file you want to process. =item - you want to hide the config file after transporting accounts? no problem: set the B<-s> argument and all config files get the HIDDEN flag by default. =item - by default that config file is named ".htaccess". you can change the name: see OPTIONS. =back =head1 USAGE perl fs2htaccess.pl [OPTIONS] DIRECTORY =head2 OPTIONS -a set READONLY flag to config files [default: off] -c CONFIG-FILE config filename [default: .htaccess] -d debug mode [default: off] -i FILE-REGEX ignore all files which match against this regular expression -s shadow config files; set hide attribute [default: off] -h print this help page -m MESSIAH messiah account(s) separated by '|' [default: Everyone|Jeder] -r process all sub-directories in DIRECTORY -v verbose mode =head2 DIRECTORY directory which will be processed. if recurse processing is on, which is by default off, all sub-directories will be processed too. s:\my\pretty\absolute\path .\relative\to\cwd =head2 CONFIG-FILE set this filename equal to the value of the AccessFileName directive in your apache config. by default this is set to '.htaccess'. per-dir-httpd.conf =head2 FILE-REGEX all files on disk matched against this regular expression will be ignored. the regular expression is processed without modificators. meta.xml ^\. .* (all files will be ignored) =head2 MESSIAH the windows system's pre-defined group 'Everyone' has different names under different locales. if that group has permission, all the folk have exactly that permission too. so the account is like a messiah cos it opens all the doors to the folks. change the MESSIAH to what ever you want, if you have more messiah than one, separete them by '|'. messiah will processed case-insensitive. example: everyone|jeder [default] =head1 SEE ALSO =begin html Win32::Perms =end html This script is only usefull if you have same user/group accounts on both, webserver and filesever. Its common to use a Domain Controller to store your user/group accounts only once. if you have a linux or unix webserver you could manage to sync the accounts from the DC with the winbind package. also you could use mod_auth_pam for your apache webserver to auth against the winbind accounts through PAM authentication. good luck... =head1 KNOWS BUGS =over 4 =item if there was an directive in an old config with other directives in side than 'require ...', these directives will bex lost when B<-i regex> matches that specific file. in short: old directives in side except 'require ...' fly away if that file should be ignored. =item script could set permission to admins on all config files. permissions could be pre-defined or given by argument -p. accounts should be given by argument B<-a>. =back please report bugs to =head1 CHANGELOG =over 4 =item from version 0.1.0 to 0.1.1 + filenames regular expressions meta chars +.()${}[]^ became escaped =back =head1 LICENSE This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. =head1 Author Copyright (C) 2005 - Markus Siebeneicher =cut ####################################################################### use strict; use warnings; use Win32; use Win32::Perms; use Win32::File; use Getopt::Std; use Benchmark; my %opts; getopts("ahvrsi:c:d", \%opts); if(defined($opts{h})) { print "USAGE: fs2htaccess.pl [OPTIONS] DIRECTORY\n"; print "\n"; print "OPTIONS:\n"; print "\t-a\t\tset READONLY flag to config files [default: off]\n"; print "\t-c CONFIG-FILE\tconfig filename [default: .htaccess]\n"; print "\t-d\t\tdebug mode [default: off]\n"; print "\t-i FILE-REGEX\tignore all files which match against this\n"; print "\t\t\tregular expression\n"; print "\t-s\t\tshadow config files; set hide attribute [default: off]\n"; print "\t-h\t\tprint this help page\n"; print "\t-m MESSIAH\tmessiah account(s) separated by '|'\n"; print "\t\t\t[default: Everyone|Jeder]\n"; print "\t-r\t\tprocess all sub-directories in DIRECTORY\n"; print "\t-v\t\tverbose mode\n"; print "\n"; print "DIRECTORY\n"; print "\tdirectory which will be processed.\n"; print "\tif recurse processing is on, which is by default off,\n"; print "\tall sub-directories will be processed too.\n"; print "\texample: s:\\my\\pretty\\absolute\\path\n"; print "\texample: .\\relative\\to\\cwd\n"; print "\n"; print "CONFIG-FILE\n"; print "\tset this filename equal to the value of the AccessFileName directive in\n"; print "\tyour apache config. by default this is set to '.htaccess'.\n"; print "\texample: per-dir-httpd.conf\n"; print "\n"; print "FILE-REGEX\n"; print "\tall files on disk matched against this regular expression will be\n"; print "\tignored. the regular expression is processed without modificators.\n"; print "\texample: meta.xml\n"; print "\texample: ^\\.\n"; print "\texample: .* (all files will be ignored)\n"; print "\n"; print "MESSIAH\n"; print "\tthe windows system's pre-defined group 'Everyone' has different\n"; print "\tnames under different locales. if that group has permission, all the\n"; print "\tfolk have exactly that permission too. so the account is like a messiah\n"; print "\tcos it opens doors to the folks. change the MESSIAH to what\n"; print "\tever you want, if you have more MESSIAH than one, separete them by '|'.\n"; print "\tMESSIAH will processed as regular expression(case-insensitive).\n"; print "\texample: Everyone|Jeder\n"; print "\n"; print "Copyright (C) 2005 Markus Siebeneicher\n"; print "This script is distributed under the General Public License.\n"; print "please report bugs to \n"; exit 0; } my $rootDirectory; if(0 > $#ARGV) { print "FATAL ERROR: missing root-directory, try with argument -h.\n"; print "exit 1"; exit 1; } elsif(0 < $#ARGV) { print "FATAL ERROR: too much arguments given or in wrong order.\n"; print "exit 1"; exit 1; } else { ($rootDirectory) = @ARGV; if($rootDirectory =~ m/[^\\]$/) { $rootDirectory.= '\\'; } # if necessary append slash } my ($recursive) = (defined($opts{r})) ? 1 : 0; our $readonly = (defined($opts{a})) ? 1 : 0; our $rwConfigAccounts = (defined($opts{p})) ? $opts{p} : ''; our $hideConfigFile = (defined($opts{s})) ? 1 : 0; our $configFilename = (defined($opts{c})) ? $opts{c} : '.htaccess'; our $ignoreFilePattern = (defined($opts{f})) ? $opts{f} : ''; our $allowedMasks = 'FILE_READ_DATA STANDARD_RIGHTS_ALL READ_CONTROL'; our $messiah = 'Everyone|Jeder'; # this string is part of a regular expression; separate messiahs by "|"; if any messiahs account has READ permission to an object all the folk will have permission to, than in fact: no permission restrictions are given # statistic vars our $countDirs = 0; our $countFiles = 0; my $t0 = new Benchmark; dirWalk($rootDirectory, $recursive); my $t1 = new Benchmark; if($opts{v}) { print "files processed: $countFiles (" . $countFiles / $countDirs . ")\n"; print "directories processed: $countDirs\n"; print "processing took: " . timestr(timediff($t1, $t0)); } sub dirWalk { my ($dir, $recursive) = @_; my $configFile = $dir.$configFilename; if(!-d $dir) { print "ERROR: given directory '$dir' is no directory.\n"; return 1; } $countDirs++; # statistic my $data = ''; # we need a defined $data string just now SOURCE is empty if(-e $configFile && !-f $configFile) { print "ERROR: config '$configFile' exists, but is not a file(-f).\n"; return 1; } elsif(-e $configFile) { # r/w mode unHideFile($configFile); # cant open hidden files cos permission denied(ask bill!) unsetReadOnly($configFile); # cant write READONLY flagged files if(!open(SOURCE, "+<", $configFile)) { # RDWR TRUNC CREAT print "ERROR: could not open '$configFile' in R/W mode: $!\n"; return 1; } while() { $data.= $_; } } else { if(!open(SOURCE, ">", $configFile)) { # only write mode print "ERROR: could not open '$configFile' in WRONLY mode: $!\n"; return 1; } } # iterate through files on disk my (@dirs, %currentFiles); # %currentFiles will be used for each found file on disk opendir(IN, $dir); # value will be hash(keys: user, group) while(defined(my $in = readdir(IN))) { next if $in =~ m/^\.\.?$/; # jump over "." and ".." # fill directory list if($recursive && -d $dir.$in) { $dirs[$#dirs+1] = $dir.$in.'\\'; } # fill files list if(-f $dir.$in && $dir.$in !~ m/$ignoreFilePattern/) { # object must be file and dont my %rdyFileAccs = prepareAccounts(getReadAccounts($dir.$in)); # match ignore pattern if($opts{d}) { print $dir.$in . ' (user: ' . $rdyFileAccs{user} . ', group: ' . $rdyFileAccs{group} . ")\n"; } $currentFiles{$in} = { %rdyFileAccs }; $countFiles++; } } closedir(IN); my %rdyDirAccs = prepareAccounts(getReadAccounts($dir)); if($opts{d}) { print $dir . '(user: '.$rdyDirAccs{user}.', group: '.$rdyDirAccs{group}.')' . "\n"; } my %foundFiles; # store found Directives from old config for my $item ($data =~ m/.*?<\/Files>/gsi) { # get all named blocks of the directive my ($file, $tmp) = $item =~ m//gi; # e.g. $file = ""; ($tmp = $file) =~ s/([\.\(\)\+\$\{\}\[\]\^])/\\\1/g; # we have to escape re meta-chars +.()${}[]^ my $directives = join( '', $item =~ m/(?<=$tmp).*(?=<\/Files>)/gis ); $directives =~ s/^\s+//mg; # remove whitespace $directives =~ s/\s+$//mg; my @directives = split( "\n", $directives ); $file =~ s//$1/; # strip filename from directive @{$foundFiles{$file}} = @directives; # but no panic: $file is free from escape chars "\" } # remove all named blocks of the Directive, next step we prebuild all current files $data =~ s/.*?<\/Files>//gsi; for my $filename ((my %tmpHash = %currentFiles)) { # iterate through %currentFiles # to build the directives with all nested directives my $directives = ''; # the %currentFiles values will no more a hash, soon string if($currentFiles{$filename}{user}) { $directives.= "\trequire user $currentFiles{$filename}{user}\n"; } # append if there are required accounts if($currentFiles{$filename}{group}) { $directives.= "\trequire group $currentFiles{$filename}{group}\n"; } if(defined($foundFiles{$filename})) { # check for nested directives in for my $oldDirective (@{$foundFiles{$filename}}) { # all found(old config) directives if($oldDirective !~ m/require/) { # and append them except: require directives $directives.= "\t". $oldDirective . "\n"; } } } if($directives) { $currentFiles{$filename} = "\n$directives"; } else { # no nested directives: no directive delete($currentFiles{$filename}); } } # from this point the script presume that there could only be 'require' directives # which arn't nested into other directives. in fact: now remove all 'require' directives # and append the fresh ones if required. $data =~ s/^.*require.*$//mig; # so we have cleaned up all that require shit, lets append new require shit again... if($rdyDirAccs{user}) { $data.= "require user $rdyDirAccs{user}\n"; } if($rdyDirAccs{group}) { $data.= "require group $rdyDirAccs{group}\n"; } while(my ($key, $value) = each(%currentFiles)) { $data.= "$value\n"; } $data =~ s/^\s+//mg; # remove empty lines $data =~ s/\s+$//mg; seek(SOURCE, 0, 0); print SOURCE $data; truncate(SOURCE, tell(SOURCE)); close(SOURCE); if(-z $configFile) { # remove empty config unlink($configFile); } else { ($hideConfigFile) ? hideFile($configFile) : unHideFile($configFile); # (un)hide config ($readonly) ? setReadOnly($configFile) : unsetReadOnly($configFile); # (un)set READONLY flag # do: set permission to specific accounts } # walk through deeper dir's my $nextDir; foreach $nextDir (@dirs) { dirWalk($nextDir, $recursive); } } sub getReadAccounts { my ($location) = @_; my @accounts; my $perms = new Win32::Perms($location); my @acl; $perms->Dump(\@acl); foreach my $ace (@acl) { # DACL(Discretionary Access Control List) required; no need for SACL(System Access Control List) next if( !defined($ace->{Entry}) || $ace->{Entry} !~ /DACL/i); my @masks; Win32::Perms::DecodeMask($ace, \@masks); foreach my $mask ( @masks ) { push( @accounts, $ace->{Account} ) if($allowedMasks =~ /$mask/i ); } } return @accounts; } # takes account-name and returns sidtype # sidtype's: 1(user), 2(group), 3(domain), 4(alias), 5(sid predefined), 6(killed account), 7(non-valid), 8(unknown) sub getAccountType { my ($account) = @_; my ($domain, $sid, $sidtype); Win32::LookupAccountName('', $account, $domain, $sid, $sidtype); return $sidtype; } # subroutine prepares an array of account-names in two groups: user and group # and returns a hash with these two elements(same name: 'user' and 'group'). # the values of the user and the group elements contain the assigned accounts # seperated by space. sub prepareAccounts { my (@preAccounts) = @_; my %accounts = (user => '', group => ''); for my $account (@preAccounts) { my $sidtype = getAccountType($account); # die guten ins töpchen, die schlechten ins kröpfchen if($sidtype == 1) { $accounts{user} .= ($accounts{user} !~ m/$account/) ? $account . ' ' : ''; } elsif($sidtype == 2) { $accounts{group} .= ($accounts{group} !~ m/$account/) ? $account . ' ' : ''; # if users 'jeder' have read permission, all user/group have read permission... # user 'jeder' is of sidtype 5(predefined) } elsif($account =~ /($messiah)/i) { $accounts{user} = ''; $accounts{group} = ''; last; } } $accounts{user} =~ s/ $//; # remove last whitespace $accounts{group} =~ s/ $//; return %accounts; } sub hideFile { my ($file) = @_; if(!-f $file) { return 1; } my $fileAttr; Win32::File::GetAttributes($file, $fileAttr); return Win32::File::SetAttributes($file, $fileAttr | HIDDEN); } sub unHideFile { my ($file) = @_; if(!-f $file) { return 1; } my $fileAttr; Win32::File::GetAttributes($file, $fileAttr); return Win32::File::SetAttributes($file, ($fileAttr | HIDDEN) - HIDDEN); } sub setReadOnly { my ($file) = @_; if(!-f $file) { return 1; } my $fileAttr; Win32::File::GetAttributes($file, $fileAttr); return Win32::File::SetAttributes($file, $fileAttr | READONLY); } sub unsetReadOnly { my ($file) = @_; if(!-f $file) { return 1; } my $fileAttr; Win32::File::GetAttributes($file, $fileAttr); return Win32::File::SetAttributes($file, ($fileAttr | READONLY) - READONLY); } exit 0;