Supporting PUT under Apache/PHP5

The following may help those who are wishing to use the HTTP PUT method
on an Apache-based server with PHP (v. 5) available.

This scenario supports access to a single server directory with a fixed
password-list. Adaptation to use a proper user/password database is left
as an exercise for the reader.

In this e-mail, sample file contents will appear between
double-bracketed statements thus:

((sample file begins))
((sample file ends))

First, a utility file to support HTTP digest authentication, which can
be located in the target directory (or a common PHP include directory if
you can set one up):

((dauth.php begins))
<?php
class auth_digest {
  private $data;

  function __construct($txt) {
    // protect against missing data
    $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1,
'username'=>1, 'uri'=>1, 'response'=>1);
    $this->data = array();

    $txt = explode(',', $txt);
    foreach ($txt as $param) {
      list($k, $v) = explode("=", $param, 2);
      $this->data[$k] = trim($v, "\"'");
      unset($needed_parts[$k]);
    }
    if ($needed_parts) throw new InvalidArgumentException($txt, 1);
  }
  function __get($k) {
    if (!isset($this->data[$k]))
      throw new InvalidArgumentException("Digest does not contain '$k'",
2);
    return $this->data[$k];
  }
  function valid_response($A1) {
    $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $this->data['uri']);
    $valid_response = md5(
      $A1 . ':' .
      $this->data['nonce'] . ':' .
      $this->data['nc'] . ':' .
      $this->data['cnonce'] . ':' .
      $this->data['qop'] . ':' . $A2
    );
    return $this->data['response'] == $valid_response;
  }
}

class authorizer {
  private $realm;
  private $users; // arrayIterator over array ('username' => 'password')
  private $plainpass; // TRUE if $users stores passwords in plain text

  function __construct($auth_realm, ArrayIterator $userIterator,
$plainpw = TRUE) {
    $this->realm = $auth_realm;
    $this->users = $userIterator;
    $this->plainpass = $plainpw;
  }
  static private function parse_digest($txt) {  // parse the http auth
header
    // protect against missing data
    $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1,
'username'=>1, 'uri'=>1, 'response'=>1);
    $data = array();

    $txt = explode(',', $txt);
    foreach ($txt as $param) {
      list($k, $v) = explode("=", $param, 2);
      $data[$k] = trim($v, "\"'");
      unset($needed_parts[$k]);
    }
    return $needed_parts ? FALSE : $data;
  }
  private static function gen_nonce() {
    return date('Ymd') . uniqid();
  }
  private static function nonceok($nonce) {
    return date('Ymd') == substr($nonce, 0, 8);
  }
  private static function unauth($status_text, $body) {
    header("HTTP/1.1 403 $status_text");
    die($body);
  }
  private function getlogin() { // dies
    header('HTTP/1.1 401 Unauthorized');
    header('WWW-Authenticate: Digest realm="' . $this->realm .
     '",qop="auth",nonce="' . self::gen_nonce() . '",opaque="' .
md5($this->realm) . '"');
    // display if user hits Cancel
    die('OK, you obviously know when you are beaten.');
  }
  function check() {
    if (empty($_SERVER['PHP_AUTH_DIGEST'])) $this->getlogin();

    // analyze the PHP_AUTH_DIGEST variable
    try {
      $d = new auth_digest($digest_text = $_SERVER['PHP_AUTH_DIGEST']);
    }
    catch (InvalidArgumentException $e) {
      self::unauth('Bad Credentials', "Parse error: $digest_text");
    }
    if (!isset($this->users[$uname = $d->username]))
      self::unauth('Unauthorized user', "Wrong credentials: user
'$uname' unknown.");

    // check valid response
    $A1 = $this->users[$uname];
    if ($this->plainpass) $A1 = md5("$uname:$this->realm:$A1");
    if (!$d->valid_response($A1))
      self::unauth('Invalid Response', "Wrong credentials:
digest=$digest_text");
    if (!self::nonceok($d->nonce)) $this->getlogin();
    return $uname;
  }
}
((dauth.php ends))

Second, in the directory concerned, place the following PHP script:

((put.php begins))
<?php
function write_log($txt) {
  file_put_contents("put_log.txt", $txt);
  chmod("put_log.txt", 0666);
}
require 'dauth.php';
// customize the following defines:-
define('AUTH_REALM', 'myserver/mydirectory');
define('URL_BASE', 'http://myserver/mydirectory');

/* ---
  The following sample authorization function uses an array of
  user names and passwords defined and fixed here.

  An actual implementation might read the array from an external
resource.
  The use of an ArrayIterator makes it moderately easy to extend this
  mechanism.
--- */

function authorize() {
  $auth = new authorizer(AUTH_REALM, new ArrayIterator(array('user1' =>
'password1', 'user2' => 'password2')));
  $auth->check(); // dies if not OK
}

function puterror($status, $body, $log = FALSE) {
  header ("HTTP/1.1 $status");
  if ($log) write_log($log);
  die("<html><head><title>Error
$status</title></head><body>$body</body></html>");
}

function putfile() {
  $f = pathinfo($fname = $_SERVER['REQUEST_URI']);
  if ($f['extension'] != 'html') puterror('403 Forbidden', "Bad file
type in $fname");
  $f = fopen($fname = $f['basename'], 'w');
  if (!$f) puterror('409 Create error', "Couldn't create file");
  $s = fopen('php://input', 'r'); // read from standard input
  if (!$s) puterror('404 Input Unavailable', "Couldn't open input");
  while($kb = fread($s, 1024)) fwrite($f, $kb, 1024);
  fclose($f);
  fclose($s);
  chmod($fname, 0666);
  $fname = URL_BASE . $fname;
  header("Location: $fname");
  header("HTTP/1.1 201 Created");
  echo "<html><head><title>Success</title></head><body>";
  echo "<p>Created <a href='$fname'>$fname</a> OK.</p></body></html>";
}

if ($_SERVER['REQUEST_METHOD'] != 'PUT')
  header("HTTP/1.1 403 Bad Request");
else {
  authorize();
  putfile();
  // uncommment the next line to debug misbehaviour
  //write_log(date('c') . "\n" . $_SERVER['REQUEST_URI'] . "\nStatus:
$retcode";
}
((put.php ends))

Third, store the following .htaccess file in the target directory,
replacing '/path-to-target-directory' with the domain-relative path
(e.g. if a target file can be read at http://mydomain/x/y/z.html, then
the /path-to-target-directory would be '/x/y'):

((.htaccess begins))
Options FollowSymLinks

RewriteEngine on
RewriteBase /path-to-target-directory
RewriteCond %{REQUEST_METHOD} !PUT
RewriteRule ^/put\.php$ - [F]
RewriteCond %{REQUEST_METHOD} PUT
RewriteCond %{QUERY_STRING} ^$
RewriteRule ^/put\.php$ - [F]
RewriteCond %{REQUEST_METHOD} PUT
RewriteRule ^(.*)$ put.php?url=$1 [L]
((.htaccess ends))

I hope this provides some clues.

Regards to all,
CPKS

Received on Saturday, 14 February 2009 06:22:35 UTC