-
-
Save openube/b532ac7dbb614e910e0d13cfb94cbfa3 to your computer and use it in GitHub Desktop.
webdav-extensions.php PHP FCGI script to Extend NGINX WebDAV by Joan LaPorte (plus some fixes from me)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
# PHP FCGI to Extend NGINX WebDAV | |
# Written by Jason LaPorte ([email protected]) | |
# | |
# Copyright (C) 2009 Agora Games, Inc. | |
# | |
# This software is provided 'as-is', without any express or implied warranty. | |
# In no event will the authors be held liable for any damages arising from the | |
# use of this software. | |
# | |
# Permission is granted to anyone to use this software for any purpose, | |
# including commercial applications, and to alter it and redistribute it | |
# freely, subject to the following restrictions: | |
# | |
# 1. The origin of this software must not be misrepresented; you must not | |
# claim that you wrote the original software. If you use this software | |
# in a product, an acknowledgment in the product documentation would be | |
# appreciated but is not required. | |
# 2. Altered source versions must be plainly marked as such, and must not be | |
# misrepresented as being the original software. | |
# 3. This notice may not be removed or altered from any source distribution. | |
# | |
# MIME type determination, miscellaneous error handling, and general code | |
# structure adapted from YUNO's similar Perl CGI script[1]. More information | |
# about this script can be found on the Agora Games website[2]. | |
# | |
# [1]: http://plan9.aichi-u.ac.jp/netlib/webappls/webdav.cgi | |
# [2]: http://blog.agoragames.com/2009/03/20/webdav-nginx-play-nice/ | |
# | |
# Version History: | |
# * 3/17/2009: V1. (Initial version.) | |
# * 3/19/2009: V2. (Added basic support for the COPY and MOVE methods. Added | |
# ZLib license for public distribution.) | |
# * 3/20/2009: V3. (Added recursive_copy and recursive_unlink functions, | |
# eliminating the need to fork processes.) | |
# | |
# To-Do: | |
# * Support (or, at least, stub out) PROPPATCH, LOCK, and UNLOCK operations. | |
# * Properly support the DEPTH header in COPY requests. | |
# * Properly support the various options for PROPFIND requests. (Ha!) | |
# Recursively delete $src. This is equivalent to UNIX's "rm -rf". Be very | |
# careful how you use it. Returns true on success and false on failure. (On | |
# failure, files that could not be deleted will obviously remain.) | |
function recursive_unlink ($src) { | |
if (file_exists ($src)) { | |
if (is_dir ($src)) { | |
foreach (scandir ($src) as $child) | |
if ($child != '.' && $child != '..') | |
recursive_unlink ("$src/$child"); | |
return rmdir ($src); | |
} | |
else | |
return unlink ($src); | |
} | |
return false; | |
} | |
# Recursively copy $src to $dest. If $dest already exists, it will be | |
# overwritten. (This makes this function not quite semantically identical to | |
# UNIX's "cp -r".) Returns true on success and false on failure. On failure, | |
# $dest may be destroyed, regardless of whether it existed before the copy or | |
# not. Thus, if you call this function, treat $dest as forfeit. | |
# FIXME: We should preserve permissions in the copy. | |
function recursive_copy ($src, $dest) { | |
if (file_exists ($src)) { | |
recursive_unlink ($dest); | |
if (is_dir ($src)) { | |
mkdir ($dest); | |
foreach (scandir ($src) as $child) | |
if ($child != '.' && $child != '..') | |
if (!recursive_copy ("$src/$child", "$dest/$child")) { | |
recursive_unlink ($dest); | |
return false; | |
} | |
return true; | |
} | |
else if (is_link ($src)) | |
return symlink (readlink ($src), $dest); | |
else | |
return copy ($src, $dest); | |
} | |
return false; | |
} | |
# Validate $key, treating it as $default if not supplied, according to the | |
# possible values in $options. | |
function validate ($key, $default, $options) { | |
if (is_null ($key) || $key === '') $key = $default; | |
return $options[$key]; | |
} | |
# Gets the MIME type of a particular file by examining its file extension. This | |
# could be greatly improved by doing something similar to the UNIX "file" | |
# command (e.g. examining headers), but this is a quick and easy hack. | |
function mime_type ($path) { | |
# I love how PHP makes me want to kill myself. Why doesn't it support | |
# nonscalar constants? | |
$mime_types = array ( | |
'aif' => 'audio/x-aiff', | |
'aiff' => 'audio/x-aiff', | |
'asc' => 'text/plain', | |
'atom' => 'text/plain', | |
'au' => 'audio/basic', | |
'avi' => 'video/x-msvideo', | |
'bmp' => 'image/bmp', | |
'c' => 'text/plain', | |
'cc' => 'text/plain', | |
'cgi' => 'text/plain', | |
'cpp' => 'text/plain', | |
'css' => 'text/css', | |
'cxx' => 'text/plain', | |
'diff' => 'text/x-diff', | |
'doc' => 'application/msword', | |
'dv' => 'video/x-dv', | |
'eps' => 'application/postscript', | |
'gif' => 'image/gif', | |
'gz' => 'application/x-gzip', | |
'h' => 'text/plain', | |
'hpp' => 'text/plain', | |
'hqx' => 'application/mac-binhex40', | |
'htm' => 'text/html', | |
'html' => 'text/html', | |
'hxx' => 'text/plain', | |
'ics' => 'text/calendar', | |
'jar' => 'application/java-archive', | |
'jav' => 'text/plain', | |
'java' => 'text/plain', | |
'jpeg' => 'image/jpeg', | |
'jpg' => 'image/jpeg', | |
'js' => 'text/plain', | |
'lzh' => 'application/x-lzh', | |
'm' => 'text/plain', | |
'm4a' => 'audio/mp4a-latm', | |
'mid' => 'audio/midi', | |
'midi' => 'audio/midi', | |
'mm' => 'text/plain', | |
'mov' => 'video/quicktime', | |
'mp2' => 'audio/mpeg', | |
'mp3' => 'audio/mpeg', | |
'mp4' => 'video/mp4', | |
'mpeg' => 'video/mpeg', | |
'mpg' => 'video/mpeg', | |
'ogg' => 'application/ogg', | |
'pdf' => 'application/pdf', | |
'php' => 'text/plain', | |
'pict' => 'image/pict', | |
'pl' => 'text/plain', | |
'png' => 'image/png', | |
'ppt' => 'application/vnd.ms-powerpoint', | |
'ps' => 'application/postscript', | |
'py' => 'text/plain', | |
'rb' => 'text/plain', | |
'rdf' => 'text/plain', | |
'rm' => 'audio/x-pn-realaudio', | |
'rtf' => 'text/rtf', | |
'sh' => 'text/plain', | |
'shtml' => 'text/html', | |
'snd' => 'audio/basic', | |
'svg' => 'image/svg+xml', | |
'swf' => 'application/x-shockwave-flash', | |
'tar' => 'application/x-tar', | |
'tex' => 'application/x-tex', | |
'tif' => 'image/tiff', | |
'tiff' => 'image/tiff', | |
'txt' => 'text/plain', | |
'vrml' => 'model/vrml', | |
'wav' => 'audio/x-wav', | |
'wbmp' => 'image/vnd.wap.wbmp', | |
'wrl' => 'model/vrml', | |
'xbm' => 'image/x-xbitmap', | |
'xhtml' => 'text/html', | |
'xls' => 'application/vnd.ms-excel', | |
'xml' => 'text/xml', | |
'xpm' => 'image/x-xpixmap', | |
'xsl' => 'text/xsl', | |
'zip' => 'application/zip' | |
); | |
$extension = substr (strrchr ($path, '.'), 1); | |
$mime = isset($mime_types[$extension]) ? $mime_types[$extension] : false; | |
return $mime ? $mime : 'application/octet-stream'; | |
} | |
# PROPFIND is recursive in nature, so it gets its own function. Since we | |
# assume "allprop" is set, it's not especially complicated: just stat() a file | |
# and format the results as XML. (Granted, the XML formatting is kinda silly, | |
# but you can blame the WebDAV folks for that.) | |
function propfind ($root, $path, $depth) { | |
$href = str_replace (array ('%2F', '+'), | |
array ('/', '%20'), | |
urlencode ($path)); | |
$file = $root . $path; | |
$exists = file_exists ($file); | |
$dir = NULL; | |
$stat = NULL; | |
if ($href === '') | |
$href = '/'; | |
if ($exists) { | |
$dir = is_dir ($file); | |
$stat = stat ($file); | |
} | |
echo ('<response>'); | |
echo ("<href>$href</href>"); | |
echo ('<propstat>'); | |
# File not found. | |
if (!$exists) | |
echo ('<status>HTTP/1.1 404 File Not Found</status>'); | |
# If we can't stat the file, it's probably a permissions issue. (I use a | |
# 403 and not a 401 because the client can never recover from the error-- | |
# it's based on the server's permissions, not the client's.) | |
else if (!$stat) | |
echo ('<status>HTTP/1.1 403 Forbidden</status>'); | |
else { | |
echo ('<status>HTTP/1.1 200 OK</status>'); | |
echo ('<prop>'); | |
$name = htmlspecialchars (basename ($file)); | |
//$created = gmdate ('c', $stat['ctime']); | |
//$modified = gmdate ('c', $stat['mtime']); | |
/* The WebDAV specification (RFC 4918) mandates that getlastmodified property uses format specified by RFC 2616 (RFC1123), | |
what is "Sun, 06 Nov 1994 08:49:37 GMT" :NELSON Source: http://winscp.net/forum/viewtopic.php?p=52871 */ | |
$created = gmdate ("D, d M Y H:i:s", $stat['ctime']).' GMT'; //NELSON: updated to RFC1123 | |
$modified = gmdate ("D, d M Y H:i:s", $stat['mtime']).' GMT'; //NELSON: updated to RFC1123 | |
# Display various general properties. | |
echo ("<displayname>$name</displayname>"); | |
echo ("<creationdate>$created</creationdate>"); | |
echo ("<getlastmodified>$modified</getlastmodified>"); | |
echo ('<supportedlock/>'); | |
# If it's a directory, say so. | |
if ($dir) | |
echo ('<resourcetype><collection/></resourcetype>'); | |
# Otherwise, print out statistics that only make sense on files. | |
else { | |
$size = $stat['size']; | |
$mime = mime_type ($path); | |
$etag = "{$stat['dev']}-{$stat['ino']}-{$stat['mtime']}"; | |
echo ('<resourcetype/>'); | |
echo ("<getcontentlength>$size</getcontentlength>"); | |
echo ("<getcontenttype>$mime</getcontenttype>"); | |
echo ("<getetag>$etag</getetag>"); | |
} | |
echo ('</prop>'); | |
} | |
echo ('</propstat>'); | |
echo ('</response>'); | |
# If this is a directory and we're set to recurse, then also print out | |
# PROPFIND responses for all of this directory's children. | |
if ($dir && $depth > 0) | |
foreach (scandir ($file) as $child) | |
if ($child != '.' && $child != '..') | |
propfind ($root, "$path/$child", $depth - 1); | |
} | |
# Response handling begins here. (Determine what method is being called, and | |
# respond appropriately.) | |
unset($_ENV); //nelson | |
$_ENV = &$_SERVER; //nelson | |
$request_method = $_ENV['REQUEST_METHOD']; | |
switch ($request_method) { | |
# PROPFIND supports a truly staggering amount of options and flags to limit | |
# or define the various pieces of data you're interested in retrieving. We | |
# pretend that the client has always specified "allprop" (that is, complete | |
# information about everything), and make it the client's responsibility to | |
# pull out less information if so desired. | |
case 'PROPFIND': | |
# Figure out what file they're looking at. | |
$document_root = $_ENV['DOCUMENT_ROOT']; | |
$document_uri = urldecode (rtrim ($_ENV['DOCUMENT_URI'], '/')); | |
#$document_uri = $_ENV['DOCUMENT_URI']=='/' ? urldecode($_ENV['DOCUMENT_URI']) : urldecode (rtrim ($_ENV['DOCUMENT_URI'], '/')); | |
# Figure out what depth to recurse to. Valid values are 0, 1, and infinity. | |
$depth = validate ($_ENV['DEPTH'], | |
'infinity', | |
array ('0' => 0, '1' => 1, 'infinity' => 'infinity')); | |
# Choke if they specify an invalid depth. | |
if (is_null ($depth)) | |
header ('HTTP/1.1 400 Bad Request'); | |
# "allprop" with an infinite depth is a scary proposition. Supporting it is | |
# both optional and stupid, so I don't. | |
else if ($depth === 'infinity') { | |
header ('HTTP/1.1 403 Forbidden'); | |
header ('Content-Type: text/xml'); | |
echo ('<error xmlns="DAV:">');// xmlns="DAV" to xmmlns:D="DAV" | |
echo ('<propfind-finite-depth/>'); | |
echo ('</error>'); | |
} | |
# Otherwise, give them the requested information. | |
else { | |
header ('HTTP/1.1 207 Multi-Status'); | |
header ('Content-Type: text/xml'); | |
echo ('<multistatus xmlns="DAV:">');// xmlns="DAV" to xmlns:D="DAV" | |
propfind ($document_root, $document_uri, $depth); | |
echo ('</multistatus>'); | |
} | |
break; | |
# We handle COPY and MOVE together, since they're almost identical. COPY | |
# should support the Depth header, which modifies the semantics of the copy. | |
# We ignore it and assume an infinite depth. Additionally, we do not support | |
# copies between servers--all copies must be local. Finally, we don't | |
# properly check for disk space errors, but a generic 500 should be good | |
# enough. | |
case 'COPY': | |
case 'MOVE': | |
# Figure out what files they're looking at. | |
$host = $_ENV['HOST']; | |
$document_root = $_ENV['DOCUMENT_ROOT']; | |
$document_uri = urldecode (rtrim ($_ENV['DOCUMENT_URI'], '/')); | |
#$document_uri = $_ENV['DOCUMENT_URI']=='/' ? urldecode($_ENV['DOCUMENT_URI']) : urldecode (rtrim ($_ENV['DOCUMENT_URI'], '/')); | |
$destination = NULL; | |
$destination_host = NULL; | |
$url = parse_url ($_ENV['DESTINATION']); | |
if ($url) { | |
$destination = urldecode (rtrim ($url['path'], '/')); | |
#$destination_host = $url['host']; | |
$destination_host = isset($url['host']) ? $url['host'] : $host; //NELSON (Fix rename file in CuteFTP) | |
} | |
$source_exists = file_exists ($document_root . $document_uri); | |
$destination_exists = file_exists ($document_root . $destination); | |
# Do we overwrite the destination file? | |
$overwrite = validate ($_ENV['OVERWRITE'], | |
'T', | |
array ('T' => true, 't' => true, | |
'F' => false, 'f' => false)); | |
# Choke if they specify an invalid destination or depth. | |
if (is_null ($destination)) | |
header ('HTTP/1.1 400 Bad Request'); | |
# Disallow copying/moving to a remote host. | |
else if ($host != $destination_host) | |
header ('HTTP/1.1 502 Bad Gateway'); | |
# Fail if the source doesn't exist. | |
else if (!$source_exists) | |
header ('HTTP/1.1 404 File Not Found'); | |
# Disallow copying/moving a file to itself. | |
else if ($document_uri == $destination) | |
header ('HTTP/1.1 403 Forbidden'); | |
# Fail if the destination file exists and they said they didn't want to | |
# overwrite it. | |
else if ($overwrite == false && $destination_exists) | |
header ('HTTP/1.1 412 Precondition Failed'); | |
# If we're doing a copy, copy the files. | |
# FIXME: We resort to shell since PHP doesn't support a recursive copy and | |
# I didn't want to bother implementing it (though it would be a good idea | |
# to do so at some point). | |
else if ($request_method == 'COPY' && | |
!recursive_copy ($document_root . $document_uri, | |
$document_root . $destination)) | |
header ('500 Internal Server Error'); | |
# If we're doing a move, move the files. | |
else if ($request_method == 'MOVE' && | |
!rename ($document_root . $document_uri, | |
$document_root . $destination)) | |
header ('500 Internal Server Error'); | |
else | |
header ($destination_exists ? | |
'HTTP/1.1 204 No Content' : | |
'HTTP/1.1 201 Created'); | |
break; | |
# In case they ask, pretend that we actually know what we're talking about. | |
# (The only methods conspicuously absent are PROPPATCH, LOCK, and UNLOCK. We | |
# tell the clients that we don't support them.) | |
case 'OPTIONS': | |
header ('HTTP/1.1 200 OK'); | |
header ('Allow: OPTIONS, GET, HEAD, POST, PUT, DELETE, MKCOL, COPY, MOVE, PROPFIND'); | |
header ('DAV: 1, 2'); | |
break; | |
# The following methods are valid but unimplemented. In theory, NGINX is | |
# supposed to handle each of them anyway. | |
case 'GET': | |
case 'HEAD': | |
case 'POST': | |
case 'PUT': | |
if (isset($_ENV['HTTP_EXPECT']) && $_ENV['HTTP_EXPECT'] == '100-continue') { | |
header ('HTTP/1.1 100 Continue'); | |
break; | |
} | |
case 'DELETE': | |
case 'MKCOL': | |
header ('HTTP/1.1 501 Not Implemented'); | |
break; | |
# Any other methods are unknown. | |
default: | |
header ('HTTP/1.1 400 Bad Request '.$request_method); | |
break; | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment