An alternative to staging domains

One of the most common practices used when developing a website is to use temporary staging address for development. The problem with this approach is that all references to the staging address need to updated to match the new address when it comes time to go live.

Here I will show you two better options. I assume that you will have some prior knowledge.

1. Hosts File

If you do not want to change the nameservers for a domain name just yet or simply do not want anyone to have access to the website while you develop it, there is an option that allows only your computer to use that address with any server you specify.

I'm not here to explain how the hosts file works, but in basic terms you can make any domain name point at a different IP address. Lets say example.com resolves to 222.222.222.222. If you were to add the following entries to your hosts file you can force your computer to see example.com as pointing at 111.111.111.111 instead of 222.222.222.222. By doing so you can build the website on any server you wish, even one installed locally.

  • 111.111.111.111 example.com
  • 111.111.111.111 www.example.com

This works with multi-domain hosts such as Apache's virtual hosts. The browser sends the target domain name as part of its request header no matter the IP used.

Where can you find the hosts file? Well on Windows it is in C: -> Windows -> System32 -> drivers -> etc. Each listing is in the following format: <ip> <domain>. You can disable entries by commenting them with a # at the beginning of the line.

What IP do you use? The one matching the server you want to develop on of course. If that is localhost you would use 127.0.0.1.

There is even a program called Hosts File Editor that makes editing your hosts file easier on Windows.

2. Proxy/Mirror Script

What happens if you want to remotely show a website to a client that you have developed using the hosts file method above or similar? Well you could setup a temporary address and go through the process of changing all the site's references to this address, but that makes the above method pointless.

A better option is to use a mirror script that automatically changes all absolute URLs to work with an alternative address. Here's how:

  1. Setup two alias addresses that are parked on the target site, e.g. dev.example.com and dev-m.example.com.
  2. Add the rewrite rules below to your .htaccess file.
  3. Change the domain name referenced in the rewrite rules to the main alias you wish to give to clients.
  4. Upload the following PHP script to the root of the site. I called mine mirror.php, if you change the name update the rewrite rules.
  5. Change the values for $base and $base2 to match the real address of the site (www and non-www version).
  6. Change the $source value to the main alias you want to give clients. e.g. dev.example.com.
  7. Change the $mirror value to the second alias you setup. e.g. dev-m.example.com.
  8. Open the main alias you are to give clients and test the site. All links should have been rewritten.

How does this work? Firstly the rewrite rules direct all requests for the main alias address to the mirror.php script. This script then connects to the second alias (which is not rewritten to mirror.php) and downloads the requested page. It finds all references of the real address used with the site within the content and replaces them with the main alias. It then sends the updated content to the client's browser. The result is that all links now work with the alias but the main website remains intact.

If you use these methods then when it comes time to go live you do not need to update any of the URLs used within the website. This is very useful especially when working with a CMS such as WordPress that can have hundreds of references to the absolute URL throughout its database.

Any questions? Hit me up in the comments below.

Rewrite Rules:

RewriteCond %{HTTP_HOST} ^dev.example.com$ [NC]
RewriteRule . mirror.php [L]

PHP Script:

<?php
$base = "www.example.com";
$base2 = "example.com.au";
$source = "dev.example.com";
$mirror = "dev-m.example.com";

$postData = "";
foreach ($_POST as $key -> $value) {
  if ($postData !== "") {
   $postData = $postData . "&";
  }
  $postData = $postData . $key . "=" . $value;
}
$data = sendToHost($source, 80, $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $postData);

header('Content-Type: ' .$data['hdrarr']['content-type']);

$contents = $data['body'];
$contents = str_ireplace($base, $mirror, $contents);
$contents = str_ireplace($base2, $mirror, $contents);

echo $contents . "<!-- Mirror: " . $_SERVER['REQUEST_URI'] . " -->";


    // [[[--- Following code is taken from http://www.phpbuilder.com/board/showthread.php?t=10348462 ---]]]

    //-------------------------------------------------------------------
    // Funciton used to decode the header used for processing body
    //
    function decode_header ( $str )
    {
        // split using optional leading CR
        $part = preg_split ( "/r?n/", $str, -1, PREG_SPLIT_NO_EMPTY );
        $out = array ();

        for ( $h = 0; $h < sizeof ( $part ); $h++ ) {
          if ( $h != 0 ) {
            // find key vs value location
            $pos = strpos ( $part[$h], ':' );
            // get key name and remove any spaces
            $k = strtolower ( str_replace ( ' ', '', substr ( $part[$h], 0, $pos ) ) );
            // get value part of data
            $v = trim ( substr ( $part[$h], ( $pos + 1 ) ) );
          }
          else {
            // setup a special status key for the status
            $k = 'status';
            $v = explode (' ', $part[$h] );
            $v = $v[1];
          }

          // check if this key is to set a cookie
          if ( $k == 'set-cookie' ) {
            // set a cookie key
            $out['cookies'][] = $v;
          }
          else {
            // save value for key
            $out[$k] = $v;
          }
        }
        // return assoicated arrary of header items
        return $out;
    }

    //-------------------------------------------------------------------
    // Funciton used to decode the body based on the encoding
    //
    //  $info is the header information obtained via decode_header
    //
    function decode_body ( $info, $str, $eol = "rn" )
    {
      $tmp = $str;
      $add = strlen ( $eol );
      $str = '';
      if ( isset ( $info['transfer-encoding'] ) && $info['transfer-encoding'] == 'chunked' ) {
        // handle removing the chunked heading data
        do {
          $tmp = ltrim ( $tmp );
          $pos = strpos ( $tmp, $eol );
          $len = hexdec ( substr ( $tmp, 0, $pos ) );
          if ( isset ( $info['content-encoding'] ) )
          {
            $str .= gzinflate ( substr ( $tmp, ( $pos + $add + 10 ), $len ) );
          }
          else
          {
            $str .= substr ( $tmp, ( $pos + $add ), $len );
          }

          $tmp = substr ( $tmp, ( $len + $pos + $add ) );
          $check = trim ( $tmp );
        } while ( ! empty ( $check ) );
      } else if ( isset ( $info['content-encoding'] ) ) {
        // decode the data
        $str = gzinflate ( substr ( $tmp, 10 ) );
      }
      else {
        // just use orginal string
        $str = $tmp;
      }
      return $str;
    }

    //-------------------------------------------------------------------
    // Funciton used to split the data obtained from remote server to
    // its header and body sections.
    //
    function parsePage(&$page, &$hdr, &$body)
    {
        $tmp = explode("rnrn", $page, 2);
        $hdr = $tmp[0];
        $body = $tmp[1];
    }

    //-------------------------------------------------------------------
    /* sendToHost - send data to remote host
    * ~~~~~~~~~~
    * Params:
    *   $host      - Just the hostname.  No http:// or
                      /path/to/file.html portions
    *   $port      - port to use (typically 80 or 443)
    *   $method    - get or post, case-insensitive
    *   $path      - The /path/to/file.html part
    *   $data      - The query string, without initial question mark
    *   $useragent - If true, 'MSIE' will be sent as
                      the User-Agent (optional)
    *
    * Examples:
    *   sendToHost('www.google.com',,'get','/search','q=php_imlib');
    *   sendToHost('www.example.com',,'post','/some_script.cgi',
    *              'param=First+Param&second=Second+param');
    */
    function sendToHost($host,$port=80,$method,$path,$data,$useragent=0)
    {
        // Supply a default method of GET if the one passed was empty
        if (empty($method)) {
            $method = 'GET';
        }
        // convert method to upper case
        $method = strtoupper($method);
        // open port
        if ($port==443) {
          $fp = fsockopen("ssl://".$host, $port);
        }
        else {
          $fp = fsockopen($host, $port);
        }
        // setup path to include variables for GET method
        if ($method == 'GET') {
            $path .= '?' . $data;
        }
        // send the http header information
        fputs($fp, "$method $path HTTP/1.1rn");
        fputs($fp, "Host: $hostrn");
        fputs($fp,"Content-type: application/x-www-form-urlencodedrn");
        if ($method=='POST') {
          fputs($fp, "Content-length: " . strlen($data) . "rn");
        }
        if ($useragent) {
            fputs($fp, "User-Agent: MSIErn");
        }
        fputs($fp, "Connection: closernrn");
        // send the post variables
        if ($method == 'POST') {
          fputs($fp, $data);
        }
        // get response from server
        while (!feof($fp)) {
          $buf .= fgets($fp,128);
        }
        // close connection
        fclose($fp);

        // separate header from body
        parsePage($buf, $hdr, $body);
        // setup array with decoded header/body
        $arr['hdrraw']=$hdr;
        $arr['hdrarr']=decode_header($hdr);
        $arr['body']=decode_body($arr['hdrarr'], $body);
        // return header and body in array
        return $arr;
    }
?>

Leave a comment