Mythos Image

Tech Blog

MySQL Apache Host Manager

Years ago I was responsible for setting up a Linux server for a company that did web design and marketing. I was the only person in this company that knew how to add additional virtual hosts into the Apache configurations. While I could have taught the employees how to do it, I really did not want to allow the Apache configuration files to be editable by people that really didn’t know what they were doing. I decided instead to pipe the configuration out to some shell scripts written in PHP that would query the local MySQL database to do virtual host look up.

I found very little documentation and example coding to help me do this, and so a lot of this came from trial and error. I’ll start with the the PHP-based shell script which I named docroots.php:

#!/usr/bin/php
<?php
$dev = "example"; # Development Domain, sans the TLD
$wwwroot = "/path/to/client/sites/"; # Base root for all client sites
$error = "/path/to/error/site"; # Base root for error page
set_time_limit(0);
$input = fopen('php://stdin', 'r');
$output = fopen('php://stdout', 'w');
while (1) {
$original = fgets($input);
$clean = strtolower(trim(preg_replace("/^www\./", "", $original)));
/* Create an array on the DNS request, e.g.
[0] = "com",
[1] = "example",
[2] = "subdomain"
for subdomain.example.com */
$request = array_reverse(explode('.', $clean));
if ($request[1] == $dev) { // If the request is in dev, use the subdomain info to deduce document root
/* Create an array on the subdomain requested, e.g.
[0] = "jsmith", (Client Name)
[1] = "store", (Site Name)
[2] = "3" (Design Choice)
for jsmith-store-3.example.com */
$client = explode ("-", $request[2]);
/* Server file organization is
/path/to/client/sites/j/jsmith/store/d3/
for the third design choice for John Smith's Online Store */
if (isset ($client[2]))
$docroot = $wwwroot.substr($client[0], 0, 1)."/".$client[0]."/".$client[1]."/d".$client[2]."/";
/* Once a design choice has been made, the design number drops off
jsmith-store.example.com
will direct Apache to a document root of
/path/to/client/sites/j/jsmith/store/dev/ */
else $docroot = $wwwroot.substr($client[0], 0, 1)."/".$client[0]."/".$client[1]."/dev/";
} else { // If the request is for the live environment, use MySQL to get the document root
require "regconnect.php"; // External file for database connection
$r = mysql_query("
SELECT
path
FROM
apache_vhosts
WHERE
host='".$clean."'
LIMIT 1;");
if ($data = mysql_fetch_array($r))
$docroot = $data[0];
else $docroot = $error; // If site not found, send to error site
mysql_close();
}
if (file_exists($docroot)) fwrite($output, $docroot . "\n");
else fwrite($output, $error . "\n");
}
?>

So to summarize this file, it predicted document roots for development sites and looked up document roots on live sites. I typically stored development environments in this manner:

/path/to/client/sites/j/jsmith/store/dev/

Then I would typically make an entry in MySQL for the live sites like this:

host path
smithexamplestore.com /path/to/client/sites/j/jsmith/store/live/

I had other columns in this table, but those are the only ones that matter for this experiment. This registered the live domain name to the proper client and site identifiers on the server. The script would then know to look for the live site’s document root at:

/path/to/client/sites/j/jsmith/store/live/

I also logged the development sites in the registry, but there was no need to look them up because it was well organized and could be deduced by the request. The required file was a very basic connect and select database script. I decided upon the procedural MySQL usage instead of the MySQLi object since I was just doing a single query then disconnecting. The regconnect.php file was simply:

<?php
mysql_connect("localhost", "registry", "password");
mysql_select_db("registry");
?>

Obviously, my the specifics were not as simplistic as these. Like all accounts that are only used by programs and services, I generated a long and complex user names and passwords that I would never remember, but was stored here in this file that was only accessible by root and the proper services, hidden away, but whose path was in the PHP configurations so it could be called easily without the path defined. Yes, a smart employee could figure it out, but they typically had access with their database accounts and the people I had on staff knew so little about *NIX that I doubt any would be able to find it.

The next thing to do is change the Apache configuration to use this script for server requests. I leveraged Apache’s Rewrite Module to accomplish this. I first create a rewrite map that is piped to my PHP script, then as long as it is not some of my global directories (for error pages or for site statistics) or robots.txt (I’ll explain how I handled this globally in another post), I piped the HTTP_HOST to the PHP Script to which its output would be the document root.

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteLock /tmp/httpd.lock
RewriteMap docroots prg:/path/to/docroots.php
RewriteCond %{REQUEST_URI} !^/error
RewriteCond %{REQUEST_URI} !^/stats
RewriteCond %{REQUEST_URI} !^/robots\.txt$
RewriteRule ^/(.*)$ ${docroots:%{HTTP_HOST}}$1
</IfModule>

This shell script worked great when I piped out the Apache configurations to it, but I did start noticing some glitches in high traffic times and was going to add some short life caching. Since every request for every resource was another database hit, but if I cached it for just a few seconds, then it would do the database hit once and all the subsequent resource requests would get satisfied by the cache. However, as I was always vary overwhelmed with more work than humanly possible to finish at that job, I never quite got around to adding it in and I never heard of any complaints from the clients.

I called this trick “dynamic virtual hosting” and it works great for referencing clients and sites with a clear organization pattern and allows for people not too good with Apache to manage virtual hosts with no need to even soft reboot the daemon.

Update

I have added caching to the docroots.php file. This is using the APC caching extension, which you must install with pecl and then you need to use the #!/usr/bin/php-cgi script parser, without the “-cgi” at the end and it will not work (I just spent the last two hours about to pull my hair out trying to figure out why the caching wasn’t working before trying this and everything started working). I also changed a couple other minor details that do not really matter to the execution of the code, just thought it read better. See updated code below:

#!/usr/bin/php-cgi
<?php
$dev = "example"; # Development Domain, sans the TLD
$wwwroot = "/path/to/client/sites/"; # Base root for all client sites
$error = "/path/to/error/site"; # Base root for error page
$cachettl = 300; # Cache time in seconds
set_time_limit(0);
$input = fopen('php://stdin', 'r');
$output = fopen('php://stdout', 'w');
while (1) {
$original = fgets($input);
$clean = strtolower(trim(preg_replace("/^www\./", "", $original)));
/* Create an array on the DNS request, e.g.
[0] = "com",
[1] = "example",
[2] = "subdomain"
for subdomain.example.com */
$request = array_reverse(explode('.', $clean));
if ($request[1] == $dev) { // If the request is in dev, use the subdomain info to deduce document root
/* Create an array on the subdomain requested, e.g.
[0] = "jsmith", (Client Name)
[1] = "store", (Site Name)
[2] = "3" (Design Choice)
for jsmith-store-3.example.com */
$client = explode ("-", $request[2]);
/* Server file organization is
/path/to/client/sites/j/jsmith/store/d3/
for the third design choice for John Smith's Online Store */
if (isset ($client[2]))
$docroot = $wwwroot.substr($client[0], 0, 1)."/".$client[0]."/".$client[1]."/d".$client[2]."/";
/* Once a design choice has been made, the design number drops off
jsmith-store.example.com
will direct Apache to a document root of
/path/to/client/sites/j/jsmith/store/dev/ */
else $docroot = $wwwroot.substr($client[0], 0, 1)."/".$client[0]."/".$client[1]."/dev/";
} else { // If the request is for the live environment, use Cache or MySQL to get the document root
if (apc_exists($clean)) $docroot = apc_fetch($clean);
else {
require "regconnect.php"; // External file for database connection
$r = mysql_query("
SELECT
path
FROM
apache_vhosts
WHERE
host='".$clean."'
LIMIT 1;");
if ($data = mysql_fetch_array($r)) {
$docroot = $data[0];
if (file_exists($docroot)) apc_add($clean, $docroot, $cachettl);
else $docroot = $error; // If site not found in file system, send to error site
} else $docroot = $error; // If site not found in database, send to error site
mysql_close();
}
}
fwrite($output, $docroot . "\n");
}
?>

If you do not use APC, you can also use an array since this is a looped script that Apache keeps running ad infinitum, but with this method, there is no time to live on the cached data and so any update you make to the registry would then have to be coupled with an Apache restart which sort of defeats one of the main reasons to go this route. However, it does still add the simplicity that virtual hosting can be managed by users that have no access to the Apache configurations and you can always setup soft reboots as a CRON job to establish a TTL. I would not advise going this route for efficiency sake, but it may still be better than repeating database hits for hosts already looked up fractions of a second earlier.

#!/usr/bin/php
<?php
$dev = "example"; # Development Domain, sans the TLD
$wwwroot = "/path/to/client/sites/"; # Base root for all client sites
$error = "/path/to/error/site"; # Base root for error page
$roots = array(); # Array to cache lookups
set_time_limit(0);
$input = fopen('php://stdin', 'r');
$output = fopen('php://stdout', 'w');
while (1) {
$original = fgets($input);
$clean = strtolower(trim(preg_replace("/^www\./", "", $original)));
/* Create an array on the DNS request, e.g.
[0] = "com",
[1] = "example",
[2] = "subdomain"
for subdomain.example.com */
$request = array_reverse(explode('.', $clean));
if ($request[1] == $dev) { // If the request is in dev, use the subdomain info to deduce document root
/* Create an array on the subdomain requested, e.g.
[0] = "jsmith", (Client Name)
[1] = "store", (Site Name)
[2] = "3" (Design Choice)
for jsmith-store-3.example.com */
$client = explode ("-", $request[2]);
/* Server file organization is
/path/to/client/sites/j/jsmith/store/d3/
for the third design choice for John Smith's Online Store */
if (isset ($client[2]))
$docroot = $wwwroot.substr($client[0], 0, 1)."/".$client[0]."/".$client[1]."/d".$client[2]."/";
/* Once a design choice has been made, the design number drops off
jsmith-store.example.com
will direct Apache to a document root of
/path/to/client/sites/j/jsmith/store/dev/ */
else $docroot = $wwwroot.substr($client[0], 0, 1)."/".$client[0]."/".$client[1]."/dev/";
} else { // If the request is for the live environment, use Cache or MySQL to get the document root
if (array_key_exists($clean, $roots)) $docroot = $roots[$clean];
else {
require "regconnect.php"; // External file for database connection
$r = mysql_query("
SELECT
path
FROM
apache_vhosts
WHERE
host='".$clean."'
LIMIT 1;");
if ($data = mysql_fetch_array($r)) {
$docroot = $data[0];
if (file_exists($docroot)) $roots[$clean] = $docroot;
else $docroot = $error; // If site not found in file system, send to error site
} else $docroot = $error; // If site not found in database, send to error site
mysql_close();
}
}
fwrite($output, $docroot . "\n");
}
?>

I was trying this method when I was frustrated over the fact that APC was not working, however for this method to really be successful, multi-threading was the best way. When the array value was made, open up a thread that sleeps for the TTL and then unsets the value from the array. Unfortunately, multi-threading in PHP also comes from external extensions so I would be faced with the same issue as APC, so ultimately figuring out the APC solution was the easier route. If extensions are really not an option, the array could be multi-dimensional where another value is set off an equation from the server’s clock to signify an expiration date on the value. Then when it finds the key in the array, if the expiration date has passed, it unsets it and does the MySQL look-up again. However, since I got the APC method to work, I didn’t bother with writing out the code for this solution, however if you need it, I have brought you most of the way there and given you the logic to take it over the line with just a slight bit more coding. If someone takes the time to code this solution, leave it in the comments below and help the next person along.

This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *