#!/bin/perl
#
# webdav.cgi - a CGI implementing the WebDAV protocol.
# (c)2006, yuno
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
$rootName = 'webdav';
$root = '/home/sugarpot';
$lock = 'webdav.lock';
#$trace = 'webdav.log';
$path = $ENV{'PATH_INFO'};
$query = $ENV{'QUERY_STRING'};
$method = $ENV{'REQUEST_METHOD'};
$depth = $ENV{'HTTP_DEPTH'};
$range = $ENV{'HTTP_RANGE'};
$ifModifiedSince = $ENV{'HTTP_IF_MODIFIED_SINCE'};
$ifRange = $ENV{'HTTP_IF_RANGE'};
$contentRange = $ENV{'HTTP_CONTENT_RANGE'};
$http_if = $ENV{'HTTP_IF'};
$locktoken = $ENV{'HTTP_LOCK_TOKEN'};
$destination = $ENV{'HTTP_DESTINATION'};
$overwrite = $ENV{'HTTP_OVERWRITE'};
$script = "http://$ENV{HTTP_HOST}$ENV{SCRIPT_NAME}";
read(STDIN, $content, $ENV{'CONTENT_LENGTH'});
@monthName = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
@wdayName = ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');
%mimeTypes = (
'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',
'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',
'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'
);
if ($trace) {
open(LOG, ">>$trace");
print LOG "----------request-------------\n";
foreach $key (sort keys %ENV) {
print LOG "$key: $ENV{$key}\n";
}
print LOG "\n$content\n";
close(LOG);
}
sub sanitize {
my $s = shift;
$s =~ s/\&/&/g;
$s =~ s/\"/"/g;
$s =~ s/\</g;
$s =~ s/\>/>/g;
$s;
}
sub urlencode {
my $s = shift;
$s =~ s/([^a-zA-Z0-9_\.\/-:])/'%'.unpack('H2', $1)/ge;
$s;
}
sub urldecode {
my $s = shift;
$s =~ s/%([A-Fa-f0-9][A-Fa-f0-9])/pack('H2', $1)/ge;
$s;
}
sub getContentType {
my $path = shift;
$path =~ tr/A-Z/a-z/;
if ($path =~ /\.([^\.]+)$/) {
if (exists $mimeTypes{$1}) {
return $mimeTypes{$1};
}
}
'application/octet-stream';
}
sub parse_xml {
local %xml = ();
local @xml_path = ();
local $xml_pcdata = '';
local %xml_namespace = ();
local @xml_namespace_stack = ();
push(@xml_path, 0);
my $content = shift;
$content =~ s/\<\?xml\s.*?\?\>//;
$content =~ s/\<(\/)?([^\s\/>]+)((\s+[^\s=]+\s*=\s*"[^"]+")*)\s*(\/)?\>|([^<>&]+)/&xml_parse_element/ge;
%xml;
}
sub xml_parse_element {
if ($6 ne '') {
$xml_pcdata .= $6;
} else {
if ($xml_pcdata ne '') {
my @path = @xml_path;
pop(@path);
my $path = join('.', @path);
$xml{"$path:#PCDATA"} .= $xml_pcdata;
$xml_pcdata = '';
}
if ($1 eq '/') {
pop(@xml_path);
&xml_namespace_pop;
} else {
my $empty_element = $5;
my $attributes = $3;
my $name = $2;
&xml_namespace_push;
push(@xml_path, pop(@xml_path) + 1);
my $path = join('.', @xml_path);
local %xml_attributes = ();
$attributes =~ s/\s+(xmlns:)?([^\s=]+)\s*=\s*"([^"]+)"/&xml_parse_attribute/ge;
if ($name =~ /([^:]+):(.*)/ && $xml_namespace{$1} ne '') {
$xml{$path} = $xml_namespace{$1}.$2;
} else {
$xml{$path} = $xml_namespace{''}.$name;
}
foreach $key (keys %xml_attributes) {
$xml{"$path:$key"} = $xml_attributes{$key};
}
if ($empty_element eq '/') {
&xml_namespace_pop;
} else {
push(@xml_path, 0);
}
}
}
}
sub xml_parse_attribute {
if ($1 ne '') {
$xml_namespace{$2} = $3;
} elsif ($2 eq 'xmlns') {
$xml_namespace{''} = $3;
} else {
$xml_attributes{$2} = $3;
}
"";
}
sub xml_namespace_push {
my @a = ();
my $key;
foreach $key (keys %xml_namespace) {
my $value = $xml_namespace{$key};
push(@a, "$key=$value");
}
push(@xml_namespace_stack, join('&', @a));
}
sub xml_namespace_pop {
my @a = split(/&/, pop(@xml_namespace_stack));
%xml_namespace = ();
foreach (@a) {
my ($key, $value) = split(/=/, $_, 2);
$xml_namespace{$key} = $value;
}
}
sub check_lock {
my $path = shift;
my $recursive = shift;
my $token;
$path .= '/';
foreach $token (keys %lock) {
if (!$if_tokens{$token}) {
my ($x, $r, $p) = split(/\t/, $lock{$token});
if ($p eq $path) {
return 0;
} elsif ($r && $path =~ /^$p/) {
return 0;
} elsif ($resursive && $p =~ /^$path/) {
return 0;
}
}
}
return 1;
}
sub get_lock {
my $path = shift;
my $exclusive = shift;
my $recursive = shift;
$path .= '/';
foreach $token (keys %lock) {
my ($x, $r, $p) = split(/\t/, $lock{$token});
if ($p eq $path) {
if ($exclusive || $x) {
return 0;
}
} elsif ($r && $path =~ /^$p/) {
if ($exclusive || $x) {
return 0;
}
} elsif ($recursive && $p =~ /^$path/) {
if ($exclusive || $x) {
return 0;
}
}
}
$lock{"<$locktoken>"} = "$exclusive\t$recursive\t$path\t";
$lockUpdated = 1;
}
sub release_lock {
my $token = shift;
delete $lock{$token};
$lockUpdated = 1;
}
sub delTree {
my $path = shift;
local @files = ();
if (-d $root.$path) {
&listFilesForDelTree($path);
sub listFilesForDelTree {
my $path = shift;
if (-r $root.$path) {
my $writable = -w $root.$path;
opendir(D, $root.$path) || die;
my @f = readdir(D);
closedir(D);
my $file;
foreach $file (@f) {
if ($file ne '.' && $file ne '..') {
my $fpath = "$path/$file";
if (!$writable) {
my $s_href = &urlencode($script.$path);
$body .= <<____;
$s_href
HTTP/1.1 403 Permission Denied
____
} elsif (!&check_lock($fpath)) {
$s_href = &urlencode($script.$path);
$body .= <<____;
$s_href
HTTP/1.1 423 Locked
____
} else {
if (-d $root.$fpath) {
&listFilesForDelTree($fpath);
}
push(@files, $root.$fpath);
}
}
}
} else {
$s_href = &urlencode($script.$path);
$body .= <<____;
$s_href/
HTTP/1.1 403 Permission Denied
____
}
}
}
push(@files, $root.$path);
if ($body ne '') {
$header{'Status'} = '207 Multi-Status';
$header{'Content-Type'} = 'text/xml; charset="utf-8"';
$body = <<____;
$body
____
0;
} else {
foreach $f (@files) {
if (-d $f) {
rmdir $f || die;
} else {
unlink $f || die;
}
}
1;
}
}
# TODO: 'not' production, etc.
$http_if =~ s/(\<[^>]+\>)/$if_tokens{$1} = 1/ge;
$path =~ s/\/+/\//g;
$path =~ s/\/$//;
$destination = &urldecode($destination);
$destination =~ s/\/$//;
open(LOCK, "+<$lock") || open(LOCK, "+>$lock") || die;
flock(LOCK, 2);
while () {
chomp;
$key = $_;
$value = ;
chomp $value;
$lock{$key} = $value;
}
$header{'Status'} = '500 Internal Server Error';
if ($path =~ /[\x00-\x1f\<\>\|]|^\.\.\/|\/\.\.\/|\/\.\.$|\/$/) {
$header{'Status'} = '403 Forbidden';
} elsif ($method eq 'OPTIONS') {
$header{'Status'} = '200 OK';
$header{'Allow'} = 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK';
$header{'DAV'} = '1, 2';
} elsif ($method eq 'GET' || $method eq 'HEAD') {
if (!-e $root.$path) {
$header{'Status'} = '404 Not Found';
} elsif (!-r $root.$path) {
$header{'Status'} = '403 Permission Denied';
} elsif (-d $root.$path) {
opendir(D, $root.$path) || die;
@files = sort readdir(D);
closedir(D);
$path .= '/';
foreach $file (@files) {
($dev, $inode, $mode, $numlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime) = stat($root.$path.$file);
($sec, $min, $hour, $day, $mon, $year, $wday) = gmtime($mtime);
$t = sprintf("%02d-%s-%4d %02d:%02d", $day, $monthName[$mon], $year + 1900, $hour, $min);
$s_ref = &urlencode($script.$path.$file);
$s_file = &sanitize($file);
if (-d $root.$path.$file) {
$s_ref .= '/';
$s_file .= '/';
$size = '-';
}
$body .= <<____;
| $s_file | $t | $size |
____
}
$s_path = &sanitize($path);
$body = <<____;
Index of $s_path
Index of $s_path
| Name | Last modified | Size |
$body
webdav.cgi (c)2006, yuno
____
$header{'Status'} = '200 OK';
$header{'Content-Type'} = 'text/html; charset="utf-8"';
$header{'Content-Length'} = length($body);
$header{'Cache-Control'} = 'public';
if ($method eq 'HEAD') {
$body = '';
}
} elsif (-f $root.$path) {
($dev, $inode, $mode, $numlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime) = stat($root.$path);
($sec, $min, $hour, $day, $mon, $year, $wday) = gmtime($mtime);
$lastmod = sprintf('%s, %2d %s %04d %02d:%02d:%02d GMT', $wdayName[$wday], $day, $monthName[$mon], $year + 1900, $hour, $min, $sec);
$header{'Status'} = '200 OK';
$header{'Content-Type'} = &getContentType($root.$path);
$header{'Last-Modified'} = $lastmod;
$header{'ETag'} = "$dev-$inode-$mtime";
if ($method eq 'HEAD') {
$header{'Content-Length'} = $size;
} elsif ($ifModifiedSince eq $lastmod) {
$header{'Status'} = '304 Not Modified';
} else {
if ($ifRange eq '' || $ifRange eq $lastmod) {
if ($range =~ /^bytes=([0-9]+)-([0-9]+)?$/) {
$first = $1;
$last = $2;
$last = $size - 1 if $last eq '' || $last >= $size;
if ($first <= $last) {
$length = $last - $first + 1;
$header{'Status'} = '206 Partial Content';
$header{'Content-Range'} = "bytes $first-$last/$size";
$partial = 1;
}
}
}
open(F, $root.$path) || die;
if ($partial) {
seek(F, $first, 0);
read(F, $body, $length);
} else {
read(F, $body, $size);
}
close(F);
$header{'Content-Length'} = length($body);
$header{'Cache-Control'} = 'public';
}
} else {
$header{'Status'} = '403 Forbidden';
}
} elsif ($method eq 'PUT') {
if (-e $root.$path && !-f $root.$path) {
$header{'Status'} = '403 Forbidden';
} elsif ($contentRange ne '') {
$header{'Status'} = '501 Not Implemented';
} elsif (!&check_lock($path)) {
$header{'Status'} = '423 Locked';
} elsif ($path =~ /(.*)\/[^\/]+$/ && !-w $root.$1) {
$header{'Status'} = '403 Permission Denied';
} else {
$overwrite = -e $root.$path;
open(F, ">$root$path") || die;
binmode(F);
print F $content;
close(F);
if (!$overwrite) {
$header{'Status'} = '201 Created';
$header{'Location'} = &urlencode($script.$path);
} else {
$header{'Status'} = '204 No Content';
}
}
} elsif ($method eq 'DELETE') {
if (!-e $root.$path) {
$header{'Status'} = '404 Not Found';
} elsif ($path eq '' || $path eq '/') {
$header{'Status'} = '403 Permission Denied';
} elsif ($path =~ /^(.*)\/[^\/]+/ && !-w $root.$1) {
$header{'Status'} = '403 Permission Denied';
} elsif (!&check_lock($path)) {
$header{'Status'} = '423 Locked';
} elsif (-d $root.$path) {
if ($depth ne '' && $depth ne 'infinity') {
$header{'Status'} = '400 Bad Request';
} elsif (&delTree($path)) {
$header{'Status'} = '204 No Content';
}
} else {
unlink($root.$path) || die;
$header{'Status'} = '204 No Content';
}
} elsif ($method eq 'MKCOL') {
if ($content ne '') {
# TODO: MKCOL with initial content
$header{'Status'} = '501 Not Implemented';
} elsif (-e $root.$path) {
$header{'Status'} = '409 Conflict';
} elsif ($path =~ /^(.*)\/[^\/]+/ && !-w $root.$1) {
$header{'Status'} = '403 Permission Denied';
} else {
mkdir($root.$path) || die;
$header{'Status'} = '201 Created';
$header{'Location'} = &urlencode($script.$path.'/');
}
} elsif ($method eq 'COPY') {
# TODO: copy method
$header{'Status'} = '501 Not Implemented';
} elsif ($method eq 'MOVE') {
if ($depth ne '' && $depth ne 'infinity') {
$header{'Status'} = '400 Bad Request';
} elsif (!&check_lock($path)) {
$header{'Status'} = '423 Locked';
} elsif ($path =~ /^(.*)\/[^\/]+$/ && !-w $root.$1) {
$header{'Status'} = '403 Permission Denied';
} elsif ($destination =~ /^$script/) {
$destpath = substr($destination, length($script));
if ($destpath =~ /^(.*)\/[^\/]+$/ && !-w $root.$1) {
$header{'Status'} = '403 Permission Denied';
} elsif ($destpath =~ /^(.*)\/[^\/]+$/ && !&check_lock($1)) {
$header{'Status'} = '423 Locked';
} elsif (!-e $root.$destpath) {
rename($root.$path, $root.$destpath) || die;
$header{'Status'} = '201 Created';
$header{'Location'} = &urlencode($script.$destpath);
} elsif ($overwrite eq 'F') {
$header{'Status'} = '412 Precondifion Failed';
} elsif (!-w $root.$destpath) {
$header{'Status'} = '403 Permission Denied';
} elsif (&delTree($destpath)) {
rename($root.$path, $root.$destpath) || die;
$header{'Status'} = '204 No Content';
}
} else {
# TODO: interserver move operation
$header{'Status'} = '501 Not Implemented';
}
} elsif ($method eq 'PROPFIND') {
$depth = 'infinity' if $depth eq '';
if ($depth ne '0' && $depth ne '1' && $depth ne 'infinity') {
$header{'Status'} = '400 Bad Request';
} elsif (!-e $root.$path) {
$header{'Status'} = '404 Not Found';
} else {
$depth = 99999 if $depth eq 'infinity';
$header{'Status'} = '207 Multi-Status';
$header{'Content-Type'} = 'text/xml; charset="utf-8"';
$body = &propfind($path, $depth);
$body = <<____;
$body
____
}
sub propfind {
my $path = shift;
my $depth = shift;
my $s_href = &urlencode($script.$path);
my ($dev, $inode, $mode, $numlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime) = stat($root.$path);
my ($sec, $min, $hour, $day, $mon, $year, $wday) = gmtime($ctime);
my $s_ctime = sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ", $year + 1900, $mon + 1, $day, $hour, $min, $sec);
($sec, $min, $hour, $day, $mon, $year, $wday) = gmtime($mtime);
$s_mtime = sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT", $wdayName[$wday], $day, $monthName[$mon], $year + 1900, $hour, $min, $sec);
$path =~ /([^\/]+)$/;
my $s_name = &sanitize($1);
if (-d $root.$path) {
my $status;
if ($depth == 0 || -r $root.$path) {
$status = '200 OK';
} else {
$status = '403 Permission Denied';
}
if ($path eq '') {
$s_name = &sanitize($rootName);
}
# TODO: lock discovery
# TODO: lock-null resource
my $body = <<____;
$s_href/
$s_name
$s_ctime
$s_mtime
HTTP/1.1 $status
____
if ($depth > 0 && opendir(D, $root.$path)) {
my @files = readdir(D);
closedir(D);
my $file;
foreach $file (@files) {
if ($file ne '.' && $file ne '..') {
$body .= &propfind("$path/$file", $depth - 1);
}
}
}
$body;
} elsif (-f $root.$path) {
my $s_type = &getContentType($root.$path);
my $body = <<____;
$s_href
$s_name
$s_ctime
$s_mtime
$size
$s_type
$dev-$inode-$mtime
HTTP/1.1 200 OK
____
$body;
}
}
} elsif ($method eq 'PROPPATCH') {
# TODO: proppatch method
$header{'Status'} = '501 Not Implemented';
} elsif ($method eq 'LOCK') {
if ($http_if eq '') {
%xml = &parse_xml($content);
$depth = 'infinity' if $depth eq '';
if ($depth ne '0' && $depth ne 'infinity') {
$header{'Status'} = '400 Bad Request';
} elsif ($xml{'1'} ne 'DAV:lockinfo') {
$header{'Status'} = '400 Bad Request';
} else {
for ($i = 1; exists $xml{"1.$i"}; ++$i) {
if ($xml{"1.$i"} eq 'DAV:lockscope') {
$lockscope = $xml{"1.$i.1"};
} elsif ($xml{"1.$i"} eq 'DAV:locktype') {
$locktype = $xml{"1.$i.1"};
} elsif ($xml{"1.$i"} eq 'DAV:owner' && $xml{"1.$i.1"} eq 'DAV:href') {
$lockowner = $xml{"1.$i.1:#PCDATA"};
}
}
if ($locktype ne 'DAV:write') {
$header{'Status'} = '501 Not Implemented';
} elsif ($lockscope ne 'DAV:exclusive' && $lockscope ne 'DAV:shared') {
$header{'Status'} = '501 Not Implemented';
} else {
$exclusive = $lockscope eq 'DAV:exclusive';
$recursive = $depth eq 'infinity';
if ($locktoken !~ /:/ || exists $lock{$locktoken}) {
$lockseq = $lock{'seq'}++;
$locktoken = $script.'/.locktoken/'.crypt(time, $lockseq);
}
if (&get_lock($path, $exclusive, $recursive)) {
$header{'Status'} = '200 OK';
$header{'Content-Type'} = 'text/xml; charset="utf-8"';
$s_scope = '' if $exclusive;
$s_scope = '' unless $exclusive;
$s_owner = &urlencode($lockowner);
$s_locktoken = &sanitize($locktoken);
$body = <<____;
$s_scope
$depth
$s_owner
Inifinite
$s_locktoken
____
} else {
$header{'Status'} = '423 Locked';
}
}
}
} else {
# TODO: refreshing lock
$header{'Status'} = '501 Not Implemented';
}
} elsif ($method eq 'UNLOCK') {
if ($locktoken =~ /:/) {
&release_lock($locktoken);
}
$header{'Status'} = '204 No Content';
} else {
$header{'Status'} = '400 Bad Request';
}
if ($lockUpdated) {
seek(LOCK, 0, 0);
truncate(LOCK, 0);
print LOCK join("\n", %lock);
}
flock(LOCK, 8);
close(LOCK);
if (!exists $header{'Content-Length'} && $body ne '') {
$header{'Content-Length'} = length($body);
}
if (!exists $header{'Content-Type'} && $header{'Content-Length'} > 0) {
$header{'Content-Type'} = 'application/octet-stream';
}
if ($trace) {
open(LOG, ">>$trace");
print LOG "------response------\n";
foreach $key (keys %header) {
$value = $header{$key};
print LOG "$key: $value\n";
}
print LOG "\n$body\n" unless $method eq 'GET';
close(LOG);
}
binmode(STDOUT);
foreach $key (keys %header) {
$value = $header{$key};
print "$key: $value\r\n";
}
print "\r\n$body";
close(STDOUT);
# end of file