offload.php
author Ryan C. Gordon <icculus@icculus.org>
Tue, 01 Oct 2013 04:05:03 +0000
changeset 142 2b13ec4e6eae
parent 87 6f6840185a24
permissions -rwxr-xr-x
Fixed compiler warning.
     1 <?php
     2 
     3 // This is a PHP script that handles offloading of bandwidth from a web
     4 //  server. It's a sort of poor-man's Akamai. It doesn't need anything
     5 //  terribly complex (Apache, PHP, a writable directory).
     6 //
     7 // It works like this:
     8 //  - You have a webserver with dynamic content, and static content that
     9 //    may change arbitrarily (i.e. - various users making changes to their
    10 //    homepages, etc). This server is under a lot of load, mostly from
    11 //    the static content, which tends to be big. There may be multiple virtual
    12 //    hosts on this machine. We call this the "base" server.
    13 //  - You have at least one other webserver that you can use to offload some
    14 //    of the bandwidth. We call this the "offload" server.
    15 //  - You set up an Apache module (mod_offload) on the first server.
    16 //    mod_offload inserts itself into the request chain, and decides if a
    17 //    given file is safe static content (real file, not a script/cgi, no
    18 //    password, etc). In those cases, it sends a 302 redirect, pointing the
    19 //    client to the offload server.
    20 //  - The offload server gets a request from the redirected client. It then
    21 //    sends an HTTP HEAD request for the file in question to the base server
    22 //    while the client waits. It decides if it has the right file based on
    23 //    the HEAD. If it does, it serves the cached file.
    24 //  - If the file is out of date, or doesn't exist on the offload server, it
    25 //    sends a regular HTTP request for it to the base server and
    26 //    begins caching it. While caching it, it also feeds it to the client
    27 //    that has been waiting.
    28 //  - If another request comes in while the file is being cached, it will
    29 //    stream what is already there from disk, and then continue to feed as
    30 //    the rest shows up.
    31 
    32 
    33 // !!! FIXME:  issues to work out.
    34 //   - Could have a partial file cached if server crashes or power goes out.
    35 //     Add a "cacher's process id" to the metadata, and have those feeding
    36 //     from the cache decide if this process died...if so, wipe the entry and
    37 //     recache it.
    38 //   - Need to have a way to clean out old files. If x.zip is on the base,
    39 //     gets cached, and then is deleted, it'll stay on the offload server
    40 //     forever. Getting a 404 from the HEAD request will clean it out, but
    41 //     the offload server needs to know to do that.
    42 
    43 
    44 //
    45 // Installation:
    46 // You need PHP with --enable-sysvsem support. You should configure PHP to not
    47 //  have a time limit on script execution (max_execution_time setting, or
    48 //  just don't run this script in safe mode and it'll handle it). PHP for
    49 //  Windows currently doesn't support sysvsem, so until someone writes me
    50 //  a mutex implementation, we assume you'll use a Unix box for this script.
    51 //
    52 // You need Apache to push every web request to this script, presumably in a
    53 //  virtual host, if not the entire server.
    54 //
    55 // Assuming this script was at /www/scripts/index.php, you would want to add
    56 //  this to Apache's config:
    57 //
    58 //   AliasMatch ^.*$ "/www/scripts/index.php"
    59 //
    60 // If you don't have control over the virtual host's config file, you can't
    61 //  use AliasMatch, but if you can put an .htaccess file in the root of the
    62 //  virtual host, you can get away with this:
    63 //
    64 //   ErrorDocument 404 /index.php
    65 //
    66 // This will make all missing files (everything) run the script, which will
    67 //  then cache and distribute the correct content, including overriding the
    68 //  404 status code with the correct one. Be careful about files that DO exist
    69 //  in that vhost directory, though. They won't offload.
    70 //
    71 // You can offload multiple base servers with one box: set up one virtual host
    72 //  on the offload server for each base server. This lets each base server
    73 //  have its own cache and configuration.
    74 //
    75 // Then edit offload_server_config.php to fit your needs.
    76 //
    77 // Restart the server so the AliasMatch configuration tweak is picked up.
    78 //
    79 // This file is written by Ryan C. Gordon (icculus@icculus.org).
    80 
    81 require_once './offload_server_config.php';
    82 require_once 'PEAR.php';
    83 
    84 define('GVERSION', '1.0.1');
    85 $GServerString = 'offload.php/' . GVERSION;
    86 
    87 $Guri = $_SERVER['REQUEST_URI'];
    88 if (strcmp($Guri{0}, '/') != 0)
    89    $Guri = '/' . $Guri;
    90 $GFilePath = NULL;
    91 $GMetaDataPath = NULL;
    92 $GSemaphore = NULL;
    93 $GSemaphoreOwned = 0;
    94 $GDebugFilePointer = NULL;
    95 $GLockDir = GOFFLOADDIR . '/lock-';
    96 $GEtagFname = NULL;
    97 
    98 function getDebugFilePointer()
    99 {
   100     global $GDebugFilePointer;
   101     if ((!GDEBUG) || (!GDEBUGTOFILE))
   102         return(NULL);
   103     if (!isset($GDebugFilePointer))
   104     {
   105         $GDebugFilePointer = fopen(GOFFLOADDIR . '/debug-' . getmypid(), 'a');
   106         if ($GDebugFilePointer === false)
   107             $GDebugFilePointer = NULL;
   108     } // if
   109     return($GDebugFilePointer);
   110 } // getDebugFilePointer
   111 
   112 
   113 function debugEcho($str)
   114 {
   115     if (GDEBUG)
   116     {
   117         if (!is_array($str))
   118             $str = $str . "\n";
   119 
   120         if (!GDEBUGTOFILE)
   121             print($str);
   122         else
   123         {
   124             $fp = getDebugFilePointer();
   125             if (isset($fp))
   126             {
   127                 @fputs($fp, print_r($str, true));
   128                 @fflush($fp);
   129             } // if
   130         } // else
   131     } // if
   132 } // debugEcho
   133 
   134 
   135 function etagToCacheFname($etag)
   136 {
   137     return(trim($etag, " \t\n\r\0\x0B\"'"));
   138 } // etagToCacheFname
   139 
   140 
   141 function getSemaphore()
   142 {
   143     global $GSemaphore, $GSemaphoreOwned, $GLockDir, $GEtagFname;
   144 
   145     debugEcho("grabbing semaphore...(owned $GSemaphoreOwned time(s).)");
   146     if ($GSemaphoreOwned++ > 0)
   147         return;
   148 
   149     if (GUSESEMAPHORE)
   150     {
   151         if (!isset($GSemaphore))
   152         {
   153             debugEcho('(have to create semaphore...)');
   154             $GSemaphore = sem_get(0x8267bc62);  // !!! FIXME: good value?
   155             if ($GSemaphore === false)
   156                 failure('503 Service Unavailable', "Couldn't allocate semaphore.");
   157         } // if
   158         sem_acquire($GSemaphore);
   159     } // if
   160     else
   161     {
   162         if ($GEtagFname == NULL)
   163             failure('503 Service Unavailable', 'Semaphore init failed');
   164 
   165         $dir = $GLockDir . $GEtagFname;
   166         $max = 100;
   167         $count = 0;
   168         while (($count < $max) && (@mkdir($dir) === false))
   169         {
   170             usleep(10000);
   171             $count++;
   172         } // while
   173 
   174         if ($count == $max)  // didn't get lock...force it. So nasty.
   175         {
   176             @rmdir($dir);
   177             $GSemaphoreOwned--;
   178             getSemaphore();
   179         } // if
   180     } // else
   181 } // getSemaphore
   182 
   183 
   184 function putSemaphore()
   185 {
   186     global $GSemaphore, $GSemaphoreOwned, $GLockDir, $GEtagFname;
   187     if ($GSemaphoreOwned == 0)
   188         return;
   189 
   190     if (--$GSemaphoreOwned == 0)
   191     {
   192         if (GUSESEMAPHORE)
   193         {
   194             if (isset($GSemaphore))
   195                 sem_release($GSemaphore);
   196         } // if
   197         else
   198         {
   199             if ($GEtagFname != NULL)
   200                 @rmdir($GLockDir . $GEtagFname);
   201         } // else
   202     } // if
   203     debugEcho("released semaphore...(now owned $GSemaphoreOwned time(s).)");
   204 } // putSemaphore
   205 
   206 
   207 function terminate()
   208 {
   209     global $GDebugFilePointer, $GSemaphoreOwned;
   210 
   211     debugEcho('offload script is terminating...');
   212     while ($GSemaphoreOwned > 0)
   213         putSemaphore();
   214 
   215     if (isset($GDebugFilePointer))
   216         @fclose($GDebugFilePointer);
   217     exit();
   218 } // terminate
   219 
   220 
   221 function doHeader($str)
   222 {
   223     if ((!GDEBUG) || (GDEBUGTOFILE))
   224     {
   225         header($str, true);
   226         if (headers_sent($filename, $linenum)) 
   227             debugEcho("Headers already sent in $filename on line $linenum");
   228     }
   229 
   230     debugEcho("header('$str');");
   231 } // doHeader
   232 
   233 
   234 function sanestrpos($haystack, $needle)
   235 {
   236     $rc = strpos($haystack, $needle);
   237     return(($rc === false) ? -1 : $rc);
   238 } // sanestrpos
   239 
   240 
   241 function loadMetadata($fname)
   242 {
   243     $retval = array();
   244     $lines = @file($fname);
   245     if ($lines === false)
   246         return($retval);
   247 
   248     $max = count($lines);
   249     for ($i = 0; $i < $max; $i += 2)
   250     {
   251         $key = trim($lines[$i]);
   252         $val = trim($lines[$i+1]);
   253         debugEcho("Loaded metadata '$key' => '$val'");
   254         $retval[$key] = $val;
   255     } // for
   256 
   257     debugEcho("Loaded $max metadata pair(s).");
   258     return($retval);
   259 } // loadMetadata
   260 
   261 
   262 function cachedMetadataMostRecent($metadata, $head)
   263 {
   264     global $GFilePath;
   265 
   266     if (!isset($metadata['Content-Length']))
   267         return(false);
   268 
   269     if (!isset($metadata['ETag']))
   270         return(false);
   271 
   272     if (!isset($metadata['Last-Modified']))
   273         return(false);
   274 
   275     if (strcmp($metadata['Content-Length'], $head['Content-Length']) != 0)
   276         return(false);
   277 
   278     if (strcmp($metadata['ETag'], $head['ETag']) != 0)
   279         return(false);
   280 
   281     if (strcmp($metadata['Last-Modified'], $head['Last-Modified']) != 0)
   282     {
   283         if (!isset($metadata['X-Offload-Is-Weak']))
   284             return(false);
   285         if (($metadata['X-Offload-Is-Weak']) == 0)
   286             return(false);
   287     } // if
   288 
   289     // See if file size != Content-Length, and if it isn't,
   290     //  see if X-Offload-Caching-PID still exists. If process
   291     //  is missing, assume transfer died and recache.
   292     $stat = @stat($GFilePath);
   293     if ($stat === false)
   294         return(false);
   295 
   296     $fsize = $stat['size'];
   297     if ($fsize != $metadata['Content-Length'])
   298     {
   299         // whoa, we were supposed to cache this!
   300         if ($metadata['X-Offload-Caching-PID'] == getmypid())
   301             return(false);
   302         else if ($metadata['X-Offload-Caching-PID'] <= 0)
   303             return(false);
   304 
   305         // !!! FIXME: Unix specific!
   306         if (!posix_kill($metadata['X-Offload-Caching-PID'], 0))
   307         {
   308             debugEcho('Caching process ID died!');
   309             return(false);
   310         } // if
   311     } // if
   312 
   313     return(true);
   314 } // cachedMetadataMostRecent
   315 
   316 
   317 function nukeRequestFromCache()
   318 {
   319     global $GMetaDataPath, $GFilePath;
   320     debugEcho('Nuking request from cache...');
   321     getSemaphore();
   322     if (isset($GMetaDataPath))
   323         @unlink($GMetaDataPath);
   324     if (isset($GFilePath))
   325         @unlink($GFilePath);
   326     putSemaphore();
   327 } // nukeRequestFromCache
   328 
   329 
   330 function failure($httperr, $errmsg, $location = NULL)
   331 {
   332     global $GServerString;
   333 
   334     if (strncasecmp($httperr, 'HTTP', 4) == 0)
   335     {
   336         $pos = sanestrpos($httperr, ' ');
   337         if ($pos >= 0)
   338             $httperr = substr($httperr, $pos+1);
   339     } // if
   340     $responseStr = "HTTP/1.0 $httperr";
   341 
   342     debugEcho('failure() called:');
   343     debugEcho('  ' . $httperr);
   344     debugEcho('  ' . $errmsg);
   345 
   346     doHeader($responseStr);
   347     doHeader('Server: ' . $GServerString);
   348     doHeader('Date: ' . HTTP::date());
   349     if (isset($location))
   350         doHeader('Location: ' . $location);
   351     doHeader('Connection: close');
   352     doHeader('Content-type: text/plain; charset=utf-8');
   353     print("$errmsg\n");
   354     terminate();
   355 } // failure
   356 
   357 function invalidContentRange($startRange, $endRange, $max)
   358 {
   359     if (($startRange < 0) || ($startRange >= $max))
   360         return(true);
   361     if (($endRange < 0) || ($endRange >= $max))
   362         return(true);
   363     if ($startRange > $endRange)
   364         return(true);
   365     return(false);
   366 } // invalidContentRange
   367 
   368 
   369 function microtime_float()
   370 {
   371    list($usec, $sec) = explode(" ", microtime());
   372    return ((float)$usec + (float)$sec);
   373 } // microtime_float
   374 
   375 
   376 function stopwatch($id = NULL)
   377 {
   378     static $storedid = NULL;
   379     static $tod = NULL;
   380 
   381     if (!GDEBUG)
   382         return;
   383 
   384     $now = microtime_float();
   385 
   386     if (isset($id))
   387         $storedid = $id;
   388 
   389     if (!isset($tod))
   390         $tod = $now;
   391     else
   392     {
   393         debugEcho("Stopwatch [$storedid]: " . ($now - $tod) . ' seconds.');
   394         $tod = NULL;
   395     } // else
   396 } // stopwatch
   397 
   398 
   399 // error handler function
   400 function myErrorHandler($errno, $errstr, $errfile, $errline)
   401 {
   402     switch ($errno)
   403     {
   404         case E_USER_ERROR:
   405             debugEcho("PHP ERROR TRIGGERED: [$errno] $errstr");
   406             debugEcho("  Fatal error in line $errline of file $errfile");
   407             debugEcho(", PHP " . PHP_VERSION . " (" . PHP_OS . ")");
   408             debugEcho("Aborting...");
   409             exit(1);
   410             break;
   411         case E_USER_WARNING:
   412             debugEcho("PHP WARNING TRIGGERED: [$errno] $errstr");
   413             break;
   414         case E_USER_NOTICE:
   415             debugEcho("PHP NOTICE TRIGGERED:</b> [$errno] $errstr");
   416             break;
   417         default:
   418             debugEcho("Unknown PHP error triggered!: [$errno] $errstr");
   419             break;
   420     } // switch
   421 } // myErrorHandler
   422 
   423 
   424 function debugInit()
   425 {
   426     global $Guri;
   427     if (GDEBUG)
   428     {
   429         header('Content-type: text/plain; charset=utf-8');
   430         debugEcho('');
   431         debugEcho('');
   432         debugEcho('');
   433         debugEcho('Offload Debug Run!');
   434         debugEcho('');
   435         debugEcho('Timestamp: ' . date('D M j G:i:s T Y'));
   436         debugEcho('Base server:' . GBASESERVER);
   437         debugEcho('User wants to get: ' . $Guri);
   438         debugEcho('Request from address: ' . $_SERVER['REMOTE_ADDR'] . '.');
   439         debugEcho('Client User-Agent: "' . $_SERVER['HTTP_USER_AGENT'] . '".');
   440         debugEcho('Referrer string: "' . $_SERVER['HTTP_REFERER'] . '".');
   441         debugEcho('Timeout for HTTP HEAD request is ' . GTIMEOUT . '.');
   442         debugEcho('Data cache goes in "' . GOFFLOADDIR . '".');
   443         debugEcho('My PID: ' . getmypid());
   444         debugEcho('');
   445         debugEcho('');
   446     } // if
   447 
   448     // force PHP errors to not go through debug system and not to user.
   449     error_reporting(E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE);
   450     set_error_handler('myErrorHandler');
   451 } // debugInit
   452 
   453 
   454 
   455 // The mainline...
   456 
   457 debugInit();
   458 
   459 // try to prevent script timeout.
   460 set_time_limit(0);
   461 
   462 // Feed a fake robots.txt to keep webcrawlers out of the offload server.
   463 if (strcmp($Guri, "/robots.txt") == 0)
   464     failure('200 OK', "User-agent: *\nDisallow: /");
   465 
   466 if (sanestrpos($Guri, '?') >= 0)
   467     failure('403 Forbidden', "Offload server doesn't do dynamic content.");
   468 
   469 $reqmethod = $_SERVER['REDIRECT_REQUEST_METHOD'];
   470 if (!isset($reqmethod)
   471     $reqmethod = $_SERVER['REQUEST_METHOD'];
   472 if (!isset($reqmethod)
   473     $reqmethod = 'GET';
   474 $ishead = (strcasecmp($reqmethod, 'HEAD') == 0);
   475 $isget = (strcasecmp($reqmethod, 'GET') == 0);
   476 
   477 if ((!ishead) && (!isget))
   478     failure('403 Forbidden', "Offload server doesn't do dynamic content.");
   479 
   480 $origurl = 'http://' . GBASESERVER . $Guri;
   481 stopwatch('HEAD transaction');
   482 $head = HTTP::head($origurl, GTIMEOUT);
   483 stopwatch();
   484 if (PEAR::isError($head))
   485     failure('503 Service Unavailable', 'Error: ' . $head->getMessage());
   486 
   487 debugEcho('The HTTP HEAD from ' . GBASESERVER . ' ...');
   488 debugEcho($head);
   489 
   490 if (($head['response_code'] == 401) || (isset($head['WWW-Authenticate'])))
   491     failure('403 Forbidden', "Offload server doesn't do protected content.");
   492 
   493 else if ($head['response_code'] != 200)
   494     failure($head['response'], $head['response'], $head['Location']);
   495 
   496 if ( (!isset($head['ETag'])) ||
   497      (!isset($head['Content-Length'])) ||
   498      (!isset($head['Last-Modified'])) )
   499 {
   500     failure('403 Forbidden', "Offload server doesn't do dynamic content.");
   501 } // if
   502 
   503 $head['X-Offload-Orig-ETag'] = $head['ETag'];
   504 $head['X-Offload-Is-Weak'] = '0';
   505 if (strlen($head['ETag']) > 2)
   506 {
   507     // a "weak" ETag?
   508     if (strncasecmp($head['ETag'], "W/", 2) == 0)
   509     {
   510         debugEcho("There's a weak ETag on this request.");
   511         $head['X-Offload-Is-Weak'] = '1';
   512         $head['ETag'] = substr($head['ETag'], 2);
   513         debugEcho('Chopped ETag to be [' . $head['ETag'] . ']');
   514     } // if
   515 } // if
   516 
   517 // !!! FIXME: Check Cache-Control, Pragma no-cache
   518 
   519 $cacheio = NULL;  // will be non-NULL if we're WRITING to the cache...
   520 $frombaseserver = false;
   521 $io = NULL;  // read from this. May be file or HTTP connection.
   522 
   523 // HTTP HEAD requests for PHP scripts otherwise run fully and throw away the
   524 //  results: http://www.figby.com/archives/2004/06/01/2004-06-01-php/
   525 if ($ishead)
   526     debugEcho('This is a HEAD request to the offload server.');
   527 
   528 // Partial content:
   529 // Does client want a range (download resume, "web accelerators", etc)?
   530 $max = $head['Content-Length'];
   531 $startRange = 0;
   532 $endRange = $max-1;
   533 $responseCode = '200 OK';
   534 $reportRange = 0;
   535 
   536 if (isset($HTTP_SERVER_VARS['HTTP_IF_RANGE']))
   537 {
   538     // !!! FIXME: handle this.
   539     $ifrange = $HTTP_SERVER_VARS['HTTP_IF_RANGE'];
   540     debugEcho("Client set If-Range: [$ifrange]...unsupported!");
   541     if (isset($HTTP_SERVER_VARS['HTTP_RANGE']))
   542         unset($HTTP_SERVER_VARS['HTTP_RANGE']);
   543 } // if
   544 
   545 if (isset($HTTP_SERVER_VARS['HTTP_RANGE']))
   546 {
   547     $range = $HTTP_SERVER_VARS['HTTP_RANGE'];
   548     debugEcho("There's a HTTP_RANGE specified: [$range].");
   549     if (strncasecmp($range, 'bytes=', 6) != 0)
   550         failure('400 Bad Request', 'Only ranges of "bytes" accepted.');
   551     else if (strpos($range, ',') !== false)
   552         failure('400 Bad Request', 'Multiple ranges not currently supported');
   553     else
   554     {
   555         $range = substr($range, 6);
   556         $pos = strpos($range, '-');
   557         if ($pos !== false)
   558         {
   559             $startRange = trim(substr($range, 0, $pos));
   560             $endRange = trim(substr($range, $pos + 1));
   561             if (strcmp($startRange, '') == 0)
   562                 $startRange = 0;
   563             if (strcmp($endRange, '') == 0)
   564                 $endRange = $max-1;
   565             $responseCode = '206 Partial Content';
   566             $reportRange = 1;
   567         } // if
   568     } // else
   569 } // if
   570 
   571 if ($endRange >= $max)  // apparently, this is legal to request.
   572     $endRange = $max - 1;
   573 
   574 debugEcho("We are feeding the client bytes $startRange to $endRange of $max");
   575 if (invalidContentRange($startRange, $endRange, $max))
   576     failure('400 Bad Request', 'Bad content range requested.');
   577 
   578 $GEtagFname = etagToCacheFname($head['ETag']);
   579 $GFilePath = GOFFLOADDIR . '/filedata-' . $GEtagFname;
   580 $GMetaDataPath = GOFFLOADDIR . '/metadata-' . $GEtagFname;
   581 $head['X-Offload-Orig-URL'] = $Guri;
   582 $head['X-Offload-Hostname'] = GBASESERVER;
   583 
   584 debugEcho('metadata cache is ' . $GMetaDataPath);
   585 debugEcho('file cache is ' . $GFilePath);
   586 
   587 if ($ishead)
   588     $metadata = $head;
   589 else
   590 {
   591     getSemaphore();
   592 
   593     $metadata = loadMetadata($GMetaDataPath);
   594     if (cachedMetadataMostRecent($metadata, $head))
   595     {
   596         $io = @fopen($GFilePath, 'rb');
   597         if ($io === false)
   598             failure('500 Internal Server Error', "Couldn't access cached data.");
   599         debugEcho('File is cached.');
   600     } // else if
   601 
   602     else
   603     {
   604         // we need to pull a new copy from the base server...
   605 
   606         ignore_user_abort(true);  // if we're caching, we MUST run to completion!
   607 
   608         $frombaseserver = true;
   609         $io = NULL;
   610         $getheaders = HTTP::get($io, $origurl, GTIMEOUT);  // !!! FIXME: may block, don't hold semaphore here!
   611         if ($io === false)
   612             failure('503 Service Unavailable', "Couldn't stream file to cache.");
   613         stream_set_blocking($io, false);
   614         stream_set_timeout($io, 60);
   615 
   616         $cacheio = @fopen($GFilePath, 'wb');
   617         if ($cacheio === false)
   618         {
   619             fclose($io);
   620             failure('500 Internal Server Error', "Couldn't update cached data.");
   621         } // if
   622 
   623         $metaout = @fopen($GMetaDataPath, 'wb');
   624         if ($metaout === false)
   625         {
   626             fclose($cacheio);
   627             fclose($io);
   628             nukeRequestFromCache();
   629             failure('500 Internal Server Error', "Couldn't update metadata.");
   630         } // if
   631 
   632         // !!! FIXME: This is a race condition...may change between HEAD
   633         // !!! FIXME:  request and actual HTTP grab. We should really
   634         // !!! FIXME:  just use this for comparison once, and if we are
   635         // !!! FIXME:  recaching, throw this out and use the headers from the
   636         // !!! FIXME:  actual HTTP grab when really updating the metadata.
   637         //
   638         // !!! FIXME: Also, write to temp file and rename in case of write failure!
   639         if (!isset($head['Content-Type']))  // make sure this is sane.
   640             $head['Content-Type'] = 'application/octet-stream';
   641 
   642         $head['X-Offload-Caching-PID'] = getmypid();
   643 
   644         foreach ($head as $key => $val)
   645             fputs($metaout, $key . "\n" . $val . "\n");
   646         fclose($metaout);
   647         $metadata = $head;
   648         debugEcho('Cache needs refresh...pulling from base server...');
   649     } // else
   650 
   651     putSemaphore();
   652 } // else
   653 
   654 doHeader('Status: ' . $responseCode);
   655 doHeader('Date: ' . HTTP::date());
   656 doHeader('Server: ' . $GServerString);
   657 doHeader('Connection: close');
   658 doHeader('ETag: ' . $metadata['ETag']);
   659 doHeader('Last-Modified: ' . $metadata['Last-Modified']);
   660 doHeader('Content-Length: ' . (($endRange - $startRange) + 1));
   661 doHeader('Accept-Ranges: bytes');
   662 doHeader('Content-Type: ' . $metadata['Content-Type']);
   663 if ($reportRange)
   664     doHeader("Content-Range: bytes $startRange-$endRange/$max");
   665 
   666 if ($ishead)
   667 {
   668     debugEcho('This was a HEAD request to offload server, so it is done.');
   669     terminate();
   670 } // if
   671 
   672 $br = 0;
   673 $endRange++;
   674 while ($br < $endRange)
   675 {
   676     $readsize = $startRange - $br;
   677     if (($readsize <= 0) || ($readsize > 8192))
   678         $readsize = 8192;
   679 
   680     if ($readsize > ($endRange - $br))
   681         $readsize = ($endRange - $br);
   682 
   683     if ($readsize == 0)
   684         break;  // Shouldn't hit, but just in case...
   685 
   686     if (feof($io))
   687     {
   688         debugEcho('feof() triggered.');
   689         break;
   690     } // if
   691 
   692     if ($frombaseserver)
   693     {
   694         $info = stream_get_meta_data($io);
   695         if ($info['eof'])
   696         {
   697             debugEcho('socket meta data has eof flag.');
   698             break;
   699         } // if
   700 
   701         else if ($info['timed_out'])
   702         {
   703             debugEcho('socket meta data has timed_out flag.');
   704             break;
   705         } // if
   706     } // if
   707 
   708     else
   709     {
   710         $stat = @fstat($io);
   711         if ($stat === false)
   712             break;
   713 
   714         $cursize = $stat['size'];
   715         if ($cursize < $max)
   716         {
   717             if (($cursize - $br) <= $readsize)  // may be caching on another process.
   718             {
   719                 sleep(1);
   720                 continue;
   721             } // if
   722         } // if
   723     } // if
   724 
   725     $data = @fread($io, $readsize);
   726     $len = strlen($data);
   727     if ($len > 0)
   728     {
   729         if (isset($cacheio))
   730         {
   731             fwrite($cacheio, $data);  // !!! FIXME: check for errors!
   732             fflush($cacheio);
   733         } // if
   734 
   735         if (!connection_aborted())
   736         {
   737             if ((!GDEBUG) || (GDEBUGTOFILE))
   738             {
   739                 if (($br >= $startRange) && ($br < $endRange))
   740                 {
   741                     $verb = GDEBUGTOFILE ? 'Wrote ' : 'Would have written ';
   742                     debugEcho($verb . $len . ' bytes.');
   743                     print($data);
   744                 } // if
   745             } // if
   746         } // if
   747         $br += $len;
   748 
   749         // If this connection is cacheing from base server, we have to keep going.
   750         if (($br == $endRange) && (isset($cacheio)) && ($br != $max))
   751         {
   752             debugEcho('Sent complete request, but am pulling from base server!');
   753             $endRange = $max;
   754         } // if
   755     } // if
   756 } // while
   757 
   758 debugEcho('Transfer is complete.');
   759 
   760 
   761 if (isset($cacheio))
   762     @fclose($cacheio);
   763 
   764 if ($br != $endRange)
   765 {
   766     debugEcho("Bogus transfer! Sent $br, wanted to send $endRange!");
   767     if ($frombaseserver)
   768         nukeRequestFromCache();
   769 } // if
   770 
   771 terminate();
   772 
   773 // end of offload script ...
   774 
   775 
   776 
   777 
   778 
   779 // This is HTTP from PEAR. Copied here for my convenience.
   780 //  I trimmed some stuff out and hacked on some other code.
   781 //    --ryan.
   782 class HTTP
   783 {
   784     static function Date($time = null)
   785     {
   786         if (!isset($time)) {
   787             $time = time();
   788         } elseif (!is_numeric($time) && (-1 === $time = strtotime($time))) {
   789             return(false);
   790         }
   791         
   792         // RFC822 or RFC850
   793         $format = ini_get('y2k_compliance') ? 'D, d M Y' : 'l, d-M-y';
   794         
   795         return gmdate($format .' H:i:s \G\M\T', $time);
   796     }
   797 
   798     function negotiateLanguage($supported, $default = 'en-US')
   799     {
   800         $supp = array();
   801         foreach ($supported as $lang => $isSupported) {
   802             if ($isSupported) {
   803                 $supp[strToLower($lang)] = $lang;
   804             }
   805         }
   806         
   807         if (!count($supp)) {
   808             return $default;
   809         }
   810 
   811         $matches = array();
   812         if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
   813             foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $lang) {
   814                 $lang = array_map('trim', explode(';', $lang));
   815                 if (isset($lang[1])) {
   816                     $l = strtolower($lang[0]);
   817                     $q = (float) str_replace('q=', '', $lang[1]);
   818                 } else {
   819                     $l = strtolower($lang[0]);
   820                     $q = null;
   821                 }
   822                 if (isset($supp[$l])) {
   823                     $matches[$l] = isset($q) ? $q : 1000 - count($matches);
   824                 }
   825             }
   826         }
   827 
   828         if (count($matches)) {
   829             asort($matches, SORT_NUMERIC);
   830             return $supp[array_pop(array_keys($matches))];
   831         }
   832         
   833         if (isset($_SERVER['REMOTE_HOST'])) {
   834             $lang = strtolower(array_pop(explode('.', $_SERVER['REMOTE_HOST'])));
   835             if (isset($supp[$lang])) {
   836                 return $supp[$lang];
   837             }
   838         }
   839 
   840         return $default;
   841     }
   842 
   843     static function head($url, $timeout = 10)
   844     {
   845         $p = parse_url($url);
   846         if (!isset($p['scheme'])) {
   847             $p = parse_url(HTTP::absoluteURI($url));
   848         } elseif ($p['scheme'] != 'http') {
   849             return HTTP::raiseError('Unsupported protocol: '. $p['scheme']);
   850         }
   851 
   852         $port = isset($p['port']) ? $p['port'] : 80;
   853 
   854         //debugEcho(array($p['host'], $port, $eno, $estr, $timeout));
   855         $fp = @fsockopen($p['host'], $port, $eno, $estr, $timeout);
   856         if ($fp === false) {
   857             if ($eno == 0) {  // dns lookup failure seems to trigger this. --ryan.
   858                 sleep(3);
   859                 $fp = @fsockopen($p['host'], $port, $eno, $estr, $timeout);
   860                 if ($fp === false) {
   861                     return HTTP::raiseError("Connection error: $estr ($eno)");
   862                 }
   863             }
   864         }
   865 
   866         $path  = !empty($p['path']) ? $p['path'] : '/';
   867         $path .= !empty($p['query']) ? '?' . $p['query'] : '';
   868 
   869         if (@fputs($fp, "HEAD $path HTTP/1.0\r\n") === false)
   870             return HTTP::raiseError("i/o error");
   871 
   872         if (@fputs($fp, 'Host: ' . $p['host'] . ':' . $port . "\r\n") === false)
   873             return HTTP::raiseError("i/o error");
   874 
   875         if (@fputs($fp, "Connection: close\r\n\r\n") === false)
   876             return HTTP::raiseError("i/o error");
   877 
   878         $response = rtrim(fgets($fp, 4096));
   879         if (preg_match("|^HTTP/[^\s]*\s(.*?)\s|", $response, $status)) {
   880             $headers['response_code'] = $status[1];
   881         }
   882         $headers['response'] = $response;
   883 
   884         while ($line = @fgets($fp, 4096)) {
   885             if (!trim($line)) {
   886                 break;
   887             }
   888             if (($pos = strpos($line, ':')) !== false) {
   889                 $header = substr($line, 0, $pos);
   890                 $value  = trim(substr($line, $pos + 1));
   891                 $headers[$header] = $value;
   892             }
   893         }
   894         fclose($fp);
   895         return $headers;
   896     }
   897 
   898     static function get(&$fp, $url, $timeout = 10)
   899     {
   900         $p = parse_url($url);
   901         if (!isset($p['scheme'])) {
   902             $p = parse_url(HTTP::absoluteURI($url));
   903         } elseif ($p['scheme'] != 'http') {
   904             return HTTP::raiseError('Unsupported protocol: '. $p['scheme']);
   905         }
   906 
   907         $port = isset($p['port']) ? $p['port'] : 80;
   908 
   909         //debugEcho(array($p['host'], $port, $eno, $estr, $timeout));
   910         $fp = @fsockopen($p['host'], $port, $eno, $estr, $timeout);
   911         if ($fp === false) {
   912             if ($eno == 0) {  // dns lookup failure seems to trigger this. --ryan.
   913                 sleep(3);
   914                 $fp = @fsockopen($p['host'], $port, $eno, $estr, $timeout);
   915                 if ($fp === false) {
   916                     return HTTP::raiseError("Connection error: $estr ($eno)");
   917                 }
   918             }
   919         }
   920 
   921         $path  = !empty($p['path']) ? $p['path'] : '/';
   922         $path .= !empty($p['query']) ? '?' . $p['query'] : '';
   923 
   924         if (@fputs($fp, "GET $path HTTP/1.0\r\n") === false)
   925             return HTTP::raiseError("i/o error");
   926 
   927         if (@fputs($fp, 'Host: ' . $p['host'] . ':' . $port . "\r\n") === false)
   928             return HTTP::raiseError("i/o error");
   929 
   930         if (@fputs($fp, "Connection: close\r\n") === false)
   931             return HTTP::raiseError("i/o error");
   932 
   933         if (@fputs($fp, "X-Mod-Offload-Bypass: true\r\n\r\n") === false)
   934             return HTTP::raiseError("i/o error");
   935 
   936         $response = rtrim(fgets($fp, 4096));
   937         if (preg_match("|^HTTP/[^\s]*\s(.*?)\s|", $response, $status)) {
   938             $headers['response_code'] = $status[1];
   939         }
   940         $headers['response'] = $response;
   941 
   942         while ($line = @fgets($fp, 4096)) {
   943             if (trim($line) == '') {
   944                 break;
   945             }
   946             if (($pos = strpos($line, ':')) !== false) {
   947                 $header = substr($line, 0, $pos);
   948                 $value  = trim(substr($line, $pos + 1));
   949                 $headers[$header] = $value;
   950             }
   951         }
   952         return $headers;
   953     }
   954 
   955     function absoluteURI($url = null, $protocol = null, $port = null)
   956     {
   957         // filter CR/LF
   958         $url = str_replace(array("\r", "\n"), ' ', $url);
   959         
   960         // Mess around with already absolute URIs
   961         if (preg_match('!^([a-z0-9]+)://!i', $url)) {
   962             if (empty($protocol) && empty($port)) {
   963                 return $url;
   964             }
   965             if (!empty($protocol)) {
   966                 $url = $protocol .':'. array_pop(explode(':', $url, 2));
   967             }
   968             if (!empty($port)) {
   969                 $url = preg_replace('!^(([a-z0-9]+)://[^/:]+)(:[\d]+)?!i', 
   970                     '\1:'. $port, $url);
   971             }
   972             return $url;
   973         }
   974             
   975         $host = 'localhost';
   976         if (!empty($_SERVER['HTTP_HOST'])) {
   977             list($host) = explode(':', $_SERVER['HTTP_HOST']);
   978         } elseif (!empty($_SERVER['SERVER_NAME'])) {
   979             list($host) = explode(':', $_SERVER['SERVER_NAME']);
   980         }
   981 
   982         if (empty($protocol)) {
   983             if (isset($_SERVER['HTTPS']) && !strcasecmp($_SERVER['HTTPS'], 'on')) {
   984                 $protocol = 'https';
   985             } else {
   986                 $protocol = 'http';
   987             }
   988             if (!isset($port) || $port != intval($port)) {
   989                 $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80;
   990             }
   991         }
   992         
   993         if ($protocol == 'http' && $port == 80) {
   994             unset($port);
   995         }
   996         if ($protocol == 'https' && $port == 443) {
   997             unset($port);
   998         }
   999 
  1000         $server = $protocol .'://'. $host . (isset($port) ? ':'. $port : '');
  1001         
  1002         if (!strlen($url)) {
  1003             $url = isset($_SERVER['REQUEST_URI']) ? 
  1004                 $_SERVER['REQUEST_URI'] : $_SERVER['PHP_SELF'];
  1005         }
  1006         
  1007         if ($url{0} == '/') {
  1008             return $server . $url;
  1009         }
  1010         
  1011         // Check for PATH_INFO
  1012         if (isset($_SERVER['PATH_INFO']) && strlen($_SERVER['PATH_INFO']) && 
  1013                 $_SERVER['PHP_SELF'] != $_SERVER['PATH_INFO']) {
  1014             $path = dirname(substr($_SERVER['PHP_SELF'], 0, -strlen($_SERVER['PATH_INFO'])));
  1015         } else {
  1016             $path = dirname($_SERVER['PHP_SELF']);
  1017         }
  1018         
  1019         if (substr($path = strtr($path, '\\', '/'), -1) != '/') {
  1020             $path .= '/';
  1021         }
  1022         
  1023         return $server . $path . $url;
  1024     }
  1025 
  1026     function raiseError($error = null, $code = null)
  1027     {
  1028         require_once 'PEAR.php';
  1029         return PEAR::raiseError($error, $code);
  1030     }
  1031 }
  1032 // end HTTP class.
  1033 
  1034 
  1035 ?>