#!/usr/bin/ruby # # Copyright (c) 2004,2013 Peter Palfrader # # All rights reserved. # require "ldap" require "getoptlong" require "myldap" require "yaml" require "etc" require 'ipaddr' @config = YAML::load( File.open( '/etc/noreply/config' ) ) ldap = MyLDAP.new(@config, "ldap2apache") clients = ldap.conn.search2(@config['basedn'], LDAP::LDAP_SCOPE_SUBTREE, 'objectclass=tnClient') # webstat @rmagicdir = @config['module']['analog']['rmagic'] @webstat_location = @config['module']['analog']['webstat'] @defaultbind = @config['module']['apache']['defaultbind'] @defaultbindhttpport = @config['module']['apache']['defaultbindhttpport'] @defaultbindhttpsport = @config['module']['apache']['defaultbindhttpsport'] @configdir = @config['module']['apache']['configdir'] @phpinipreamble = @config['module']['apache']['phpinipreamble'] @phpwrapperfilesdir = @config['module']['apache']['phpwrapperfilesdir'] @phpinifilesdir = @config['module']['apache']['phpinifilesdir'] @postgres_uid = @config['module']['apache']['postgres_uid'] @www_gid = @config['module']['apache']['www_gid'] @configtest = @config['module']['apache']['configtest'] @reload = @config['module']['apache']['reload'] @restart = @config['module']['apache']['restart'] @statsbind = @config['module']['apache']['statsbind'] @statsvhostaddto = @config['module']['apache']['statsvhostaddto'] files = {} phpinifiles = {} phpwrapperfiles = {} def mkdir(dir, mode, uid, gid) Dir.mkdir(dir, mode) unless File.exists?(dir) # unless file is a symlink unless FileTest.symlink?(dir) File.chown(uid, gid, dir) == 1 or throw "chown #{dir} failed" chmodded = File.chmod(mode, dir) throw "Could not chmod #{dir} to #{mode}" unless chmodded == 1 end end def make_vhostline(addresses, port) line = [] line << "" return line.join(' ') end clients.each{ |c| c['vhosts'] = ldap.conn.search2(c['dn'][0], LDAP::LDAP_SCOPE_SUBTREE, '(&(objectclass=tnWebVHost)(tnHost='+@config['thishost']+'))') client_home = c['homeDirectory'][0] or throw "No home for #{d['dn'][0]}" uid = c['uidNumber'][0].to_i gid = c['gidNumber'][0].to_i mkdir(client_home , 02755, 0, 0) mkdir(client_home+"/logs" , 02755, 0, gid) mkdir(client_home+"/logs-archive" , 02755, 0, gid) mkdir(client_home+"/logs-for-stat", 02750, 0, gid) mkdir(client_home+"/pg" , 02750, @postgres_uid, gid) c['vhosts'].each{ |vhost| bind = vhost['tnWebVHostBind'] ? vhost['tnWebVHostBind'] : @defaultbind bind = [ bind ] unless bind.kind_of?(Array) bindhttpport = vhost['tnWebVHostBindHTTPPort'] ? vhost['tnWebVHostBindHTTPPort'][0] : @defaultbindhttpport bindhttpsport = vhost['tnWebVHostBindHTTPSPort'] ? vhost['tnWebVHostBindHTTPSPort'][0] : @defaultbindhttpsport server_name = vhost['tnWebVHostServerName'][0] server_admin = vhost['tnWebVHostWebmaster'][0] server_aliases = (vhost['tnWebVHostServerAlias'] or []).join(" ") home = client_home +"/"+ vhost['tnWebVHostHomeDirectory'][0] property = { # list multi-value keys here 'phpiniaddon' => [] } if vhost['tnWebVHostProperties'] vhost['tnWebVHostProperties'].each{ |prop| (key, val) = prop.split('=', 2) if property.has_key? key if property[key].kind_of?(Array) property[key] << val else throw "Found tnWebVHostProperties entry '#{prop}' but #{key} is already #{property[key]}" end else property[key] = val end } end property['php'] = "no" unless %w(yes no cgi5).include?property['php'] dirindex = vhost['tnWebVHostDirectoryIndex'][0] if vhost['tnWebVHostDirectoryIndex'] docdiroptions = (vhost['tnWebVHostDocDirOptions'] or []).join(" ") cgidiroptions = (vhost['tnWebVHostCgiDirOptions'] or []).join(" ") docdiroptions += " Includes" if property['ssi'] == "yes" addto = vhost['tnWebVHostAddto'][0].gsub("\n", "\n\t") if vhost['tnWebVHostAddto'] docdiraddto = vhost['tnWebVHostDocDirAddto'][0].gsub("\n", "\n\t") if vhost['tnWebVHostDocDirAddto'] cgidiraddto = vhost['tnWebVHostCgiDirAddto'][0].gsub("\n", "\n\t") if vhost['tnWebVHostCgiDirAddto'] if property['https'] == "only" ssl = true http_ssl_upgrade = true unless property['http_ssl_upgrade'] == "no" end vhostuid = uid vhostgid = gid if property['vhostUser'] u = Etc.getpwnam(property['vhostUser']) vhostuid = u['uid'] vhostgid = u['gid'] end if property['vhostGroup'] g = Etc.getgrnam(property['vhostGroup']) vhostgid = g['gid'] end vhostuidname = Etc.getpwuid(vhostuid)['name'] vhostgidname = Etc.getgrgid(vhostgid)['name'] umask=File.umask File.umask(0000) if property['permissionstyle'] == "new" mkdir(home, 00750, vhostuid, @www_gid) else mkdir(home, 02755, vhostuid, vhostgid) end if vhostgid != gid mkdir(home+"/htdocs", 02775, vhostuid, vhostgid) mkdir(home+"/bin", 02775, 0, vhostgid) else mkdir(home+"/htdocs", 02755, vhostuid, vhostgid) mkdir(home+"/bin", 02755, 0, vhostgid) end mkdir(home+"/cgi-bin", 02755, vhostuid, vhostgid) if property['cgi-bin'] == "yes" if property['php'] == "cgi5" mkdir(home+"/tmp", 00770, vhostuid, vhostgid) elsif property['php']!="no" mkdir(home+"/tmp", 03775, vhostuid, @www_gid) else Dir.rmdir( home+"/tmp" ) if File.exists?(home+"/tmp") end File.umask(umask) config = [] basedir = nil unless property['php'] == "cgi5" basedir = "#{home}" if property['open_basedir'] if property['open_basedir'] != "none" basedir += ":" + property['open_basedir'] else basedir = nil end end if property['safemode_include'] safemode_include = property['safemode_include'] end end phpincludepath = [ '.', "#{home}/include", "/usr/share/php", "/usr/share/pear", "/usr/share/php/smarty/libs" ].map { |d| File.exists?(d) ? d : nil }.compact.join ':' if ssl crtfile = "/etc/ssl/certs/apache-#{server_name}.pem" chainfile = "/etc/ssl/certs/apache-#{server_name}-chain.pem" keyfile = "/etc/ssl/private/apache-#{server_name}.key" STDERR.puts "Warning: #{crtfile} does not exist" unless FileTest.exists?(crtfile) STDERR.puts "Warning: #{keyfile} does not exist" unless FileTest.exists?(keyfile) config << make_vhostline(bind, bindhttpsport) config << " SSLEngine on" config << " SSLCertificateFile #{crtfile}" config << " SSLCertificateKeyFile #{keyfile}" config << " SSLCertificateChainFile #{chainfile}" if FileTest.exists?(chainfile) config << ' ' config << ' SSLOptions +StdEnvVars' config << ' ' config << ' SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown' config << '' else config << make_vhostline(bind, bindhttpport) end config << " ServerName #{server_name}" config << " ServerAlias #{server_aliases}" if server_aliases != "" config << " ServerAdmin #{server_admin}" config << "" if property['canonical'] == "yes" config << " RewriteEngine on" config << " RewriteCond %{HTTP_HOST} !^#{server_name}$" config << " RewriteRule ^/(.*) %s://#{server_name}/$1 [R]"%[ ( ssl ? 'https' : 'http' ) ] end config << "" unless property['userdir'] == "yes" config << " " config << " UserDir disabled" config << " " end config << " SuexecUserGroup #{vhostuidname} #{vhostgidname}" unless property['setusergroup'] == "no" config << " ErrorLog #{client_home}/logs/#{server_name}-error.log" config << " LogLevel warn" config << " CustomLog #{client_home}/logs/#{server_name}-access.log combined" config << " ServerSignature On" config << "" config << "" if property['php'] == "yes" config << " " config << " php_admin_value engine 1" unless property['safe_mode'] == "no" config << " php_admin_value safe_mode 1" config << " php_admin_value safe_mode_gid 1" config << " php_admin_value safe_mode_exec_dir \"#{home}/bin\"" end config << " php_admin_value upload_tmp_dir \"#{home}/tmp\"" config << " php_admin_value sendmail_path \"/usr/sbin/sendmail -t -i -f #{server_admin}\"" config << " php_admin_value allow_url_fopen 0" config << " " elsif property['php'] == "no" config << " " config << " php_admin_value engine off" config << " " config << " AddType text/plain .php3" config << " AddType text/plain .php3s" config << " AddType text/plain .php" config << " AddType text/plain .php4" config << " AddType text/plain .phps" end unless property['document_root'] == "manual" config << " DocumentRoot #{home}/htdocs" end config << " " config << " #AllowOverride FileInfo AuthConfig Limit Indexes Options" config << " IndexOptions FancyIndexing NameWidth=*" if property['php'] == "yes" config << " " config << " php_value magic_quotes_gpc 0" config << " php_admin_value open_basedir \"#{basedir}\"" if basedir config << " php_admin_value safe_mode_include_dir \"#{safemode_include}\"" if safemode_include config << " php_value include_path \"#{phpincludepath}\"" config << " " end config << " Options #{docdiroptions}" if docdiroptions != "" config << " #{docdiraddto}" if docdiraddto config << " DirectoryIndex #{dirindex}" if dirindex config << " AddHandler server-parsed .html" if property['ssi'] == 'yes' config << " AddType text/html .shtml" if property['ssi'] == 'yes' config << " AddHandler server-parsed .shtml" if property['ssi'] == 'yes' config << "" config << " Order allow,deny" if property['allowfrom'] property['allowfrom'].split(/ */).each do |l| config << " Allow from #{l}" end else config << " Allow from all" end config << " " if property['php'] == "cgi5" throw "@config['module']['apache']['phpwrapperfilesdir'] is not defined, yet we use php-cgi" unless @phpwrapperfilesdir if property['open_basedir'] or property['safemode_include'] STDERR.puts "Warning: open_basedir and safemode_include not supported with php-cgi" end phpini = [] phpini.concat IO.readlines(@phpinipreamble).collect{|a| a.chop} if @phpinipreamble and FileTest.exists?(@phpinipreamble) phpini << "[PHP]" phpini << "upload_tmp_dir = \"#{home}/tmp\"" phpini << "sendmail_path = \"/usr/sbin/sendmail -t -i -f #{server_admin}\"" phpini << "allow_url_fopen = 0" phpini << "magic_quotes_gpc = 0" phpini << "include_path = \"#{phpincludepath}\"" phpini << property['phpiniaddon'] if property['phpiniaddon'] inilp = c['o'][0] +"-"+ server_name throw "Clash on #{server_name} of client#{c['o'][0]} for phpinifiles" if phpinifiles[ inilp ] phpinifiles[ inilp ] = phpini wrap = [] wrap << "#!/bin/bash" wrap << "" wrap << "exec /usr/bin/php5-cgi --php-ini #{@phpinifilesdir}/#{inilp} \"$@\"" wraplp1 = "#{vhostuid}=#{vhostgid}" wraplp2 = "php-#{server_name}" phpwrapperfiles[wraplp1] = {} unless phpwrapperfiles[wraplp1] throw "Clash on #{server_name} of client#{c['o'][0]} for phpwrapperfiles" if phpwrapperfiles[wraplp1][wraplp2] phpwrapperfiles[wraplp1][wraplp2] = wrap phppath = "#{@phpwrapperfilesdir}/#{wraplp1}/#{wraplp2}" config << " " config << " AddHandler fcgid-script .php" config << " FCGIWrapper #{phppath} .php" config << " " config << " Options ExecCGI" config << " " config << " " end if property['cgi-bin'] == "yes" cgihome = home.gsub(/^\/srv\/www\/vhosts/, '/var/www/vhosts') config << " ScriptAlias /cgi-bin #{cgihome}/cgi-bin" config << " " config << " AllowOverride FileInfo AuthConfig Limit Indexes" config << " Options ExecCGI #{cgidiroptions}" config << " #{cgidiraddto}" if cgidiraddto config << " " end config << " #{addto}" if addto # # webstats, part I - proxy to our second webserver unless property['stats'] == "no" config << ' Alias /awstats-classes/ "/usr/share/awstats/classes/"' config << ' Alias /awstats-css/ "/usr/share/awstats/css/"' config << ' Alias /awstats-icon/ "/usr/share/awstats/icon/"' config << " " config << " ProxyPass /webstats/ http://#{@statsbind}/" config << " ProxyPassReverse /webstats/ http://#{@statsbind}/" config << " ProxyPreserveHost on" config << " " end config << "" config << '# vim:ft=apache:' throw "Clash on #{server_name} of client#{c['o'][0]}" if files[ c['o'][0] +"-"+ server_name ] files[ c['o'][0] +"-"+ server_name ] = config if ssl and http_ssl_upgrade config = [''] config << make_vhostline(bind, bindhttpport) config << " ServerName #{server_name}" config << " ServerAlias #{server_aliases}" if server_aliases != "" config << " ServerAdmin #{server_admin}" config << " " config << " UserDir disabled" config << " " config << " ErrorLog #{client_home}/logs/#{server_name}-error.log" config << " LogLevel warn" config << " CustomLog #{client_home}/logs/#{server_name}-access.log combined" config << " ServerSignature On" config << " RewriteEngine on" config << " RewriteRule ^/(.*)$ https://#{server_name}"+(bindhttpsport == 443 ? '' : ":#{bindhttpsport}")+"/$1 [L,R]" config << "" config << '# vim:ft=apache:' files[ c['o'][0] +"-"+ server_name ] += config end # # webstats, part II - we need our own vhost since we can't have suexec enabled unless property['stats'] == "no" config = [''] config << "" config << " ServerName #{server_name}" config << " ServerAdmin #{server_admin}" config << " " config << " UserDir disabled" config << " " config << "# ErrorLog #{client_home}/logs/stats.#{server_name}-error.log" config << "# LogLevel warn" config << "# CustomLog #{client_home}/logs/stats.#{server_name}-access.log combined" config << " ServerSignature On" config << "" config << ' Alias /awstats-classes/ "/usr/share/awstats/classes/"' config << ' Alias /awstats-css/ "/usr/share/awstats/css/"' config << ' Alias /awstats-icon/ "/usr/share/awstats/icon/"' config << " ScriptAlias /awstats.pl /usr/lib/cgi-bin/awstats.pl" config << " ScriptAlias / /usr/lib/cgi-bin/awstats.pl" config << "" config << " #{@statsvhostaddto}" if @statsvhostaddto config << "" config << '# vim:ft=apache:' files[ c['o'][0] +"-"+ server_name ] += config end } } def write_files(basedir, f, mode=0644, uid=nil, gid=nil) need_reload = false Dir.entries( basedir ).each{ |e| next if ((e =~ /^\./) != nil) next if f.has_key?( e ) File.unlink( basedir + '/' + e ) need_reload = true } f.each_pair{ |name, config| filename = basedir + '/' + name conf = config.join("\n") + "\n" on_disk = FileTest.exists?(filename) ? File.read(filename) : nil next if on_disk == conf f = File.new( filename, "w" ) f.chmod(mode) f.write(conf) f.close File.chown(uid, gid, filename) if uid or gid need_reload = true } return need_reload end if phpinifiles.size > 0 throw "@config['module']['apache']['phpinifilesdir'] is not defined, yet we use php-cgi" unless @phpinifilesdir throw "@config['module']['apache']['restart'] is not defined, yet we use php-cgi" unless @restart end need_reload = write_files(@configdir, files) need_restart = write_files(@phpinifilesdir, phpinifiles) if @phpinifilesdir phpwrapperfilesdirectories = {} phpwrapperfiles.each_pair do |dirname, wrappers| m = /^(.*)=(.*)$/.match dirname throw "Could not parse phpwrapperfilesdir dirname #{dirname}" unless m dir = @phpwrapperfilesdir + '/' + dirname mkdir(dir, 0755, m[1].to_i, m[2].to_i) unless File.exists?(dir) write_files(dir, wrappers, 0755, m[1].to_i, m[2].to_i) phpwrapperfilesdirectories[dirname] = true end if @phpwrapperfilesdir Dir.entries( @phpwrapperfilesdir ).each do |e| next if ((e =~ /^\./) != nil) next if phpwrapperfilesdirectories.has_key?( e ) Dir.entries( @phpwrapperfilesdir+'/'+e ).each do |e2| next if ((e2 =~ /^\./) != nil) and File.directory?(@phpwrapperfilesdir+'/'+e+'/'+e2) File.unlink( @phpwrapperfilesdir+'/'+e+'/'+e2 ) end Dir.rmdir( @phpwrapperfilesdir+'/'+e ) need_reload = true end end if need_restart system(@configtest) or throw "Configtest returned errors." system(@restart) or throw "Restart returned errors." elsif need_reload system(@configtest) or throw "Configtest returned errors." system(@reload) or throw "Reload returned errors." end