PageRenderTime 64ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Net/DAV/Server.pm

https://github.com/gitpan/Net-DAV-Server
Perl | 961 lines | 779 code | 144 blank | 38 comment | 110 complexity | 2cfd2fa09aa864ab478ddc8f5e2c6e0c MD5 | raw file
  1. package Net::DAV::Server;
  2. use strict;
  3. use warnings;
  4. use File::Slurp;
  5. use Encode;
  6. use File::Find::Rule::Filesys::Virtual;
  7. use HTTP::Date qw(time2str time2isoz);
  8. use HTTP::Headers;
  9. use HTTP::Response;
  10. use HTTP::Request;
  11. use File::Spec;
  12. use URI;
  13. use URI::Escape;
  14. use XML::LibXML;
  15. use XML::LibXML::XPathContext;
  16. use Net::DAV::LockManager ();
  17. use Net::DAV::LockManager::DB ();
  18. our $VERSION = '1.305';
  19. $VERSION = eval $VERSION; # convert development version into a simpler version number.
  20. our %implemented = (
  21. options => 1,
  22. put => 1,
  23. get => 1,
  24. head => 1,
  25. post => 1,
  26. delete => 1,
  27. mkcol => 1,
  28. propfind => 1,
  29. copy => 1,
  30. lock => 1,
  31. unlock => 1,
  32. move => 1
  33. );
  34. sub new {
  35. my $class = shift;
  36. my %args = @_ % 2 ? () : @_;
  37. my $self = {};
  38. if ( $args{'-dbobj'} ) {
  39. $self->{'lock_manager'} = Net::DAV::LockManager->new( $args{'-dbobj'} );
  40. }
  41. elsif ( $args{'-dbfile'} ) {
  42. $self->{'_dsn'} = "dbi:SQLite:dbname=$args{'-dbfile'}";
  43. }
  44. elsif ( $args{'-dsn'} ) {
  45. $self->{'_dsn'} = $args{'-dsn'};
  46. }
  47. bless $self, $class;
  48. if ( $args{'-filesys'} ) {
  49. $self->filesys( $args{'-filesys'} );
  50. }
  51. return $self;
  52. }
  53. sub filesys {
  54. my ($self, $nfs) = @_;
  55. $self->{'-filesys'} = $nfs if defined $nfs;
  56. return $self->{'-filesys'};
  57. }
  58. sub run {
  59. my ( $self, $request, $response ) = @_;
  60. my $fs = $self->filesys || die 'Filesys missing';
  61. my $method = $request->method;
  62. my $path = uri_unescape $request->uri->path;
  63. if ( !defined $response ) {
  64. $response = HTTP::Response->new;
  65. }
  66. $method = lc $method;
  67. if ( $implemented{$method} ) {
  68. $response->code(200);
  69. $response->message('OK');
  70. eval {
  71. $response = $self->$method( $request, $response );
  72. $response->header( 'Content-Length' => length( $response->content ) ) if defined $response->content;
  73. 1;
  74. } or do {
  75. return HTTP::Response->new( 400, 'Bad Request' );
  76. };
  77. }
  78. else {
  79. # Saying it isn't implemented is better than crashing!
  80. $response->code(501);
  81. $response->message('Not Implemented');
  82. }
  83. return $response;
  84. }
  85. sub options {
  86. my ( $self, $request, $response ) = @_;
  87. $response->header( 'DAV' => '1,2,<http://apache.org/dav/propset/fs/1>' ); # Nautilus freaks out
  88. $response->header( 'MS-Author-Via' => 'DAV' ); # Nautilus freaks out
  89. $response->header( 'Allow' => join( ',', map { uc } keys %implemented ) );
  90. $response->header( 'Content-Type' => 'httpd/unix-directory' );
  91. $response->header( 'Keep-Alive' => 'timeout=15, max=96' );
  92. return $response;
  93. }
  94. sub head {
  95. my ( $self, $request, $response ) = @_;
  96. my $path = uri_unescape $request->uri->path;
  97. my $fs = $self->filesys;
  98. if ( $fs->test( 'f', $path ) && $fs->test( 'r', $path ) ) {
  99. $response->last_modified( $fs->modtime($path) );
  100. }
  101. elsif ( $fs->test( 'd', $path ) ) {
  102. $response->header( 'Content-Type' => 'text/html; charset="utf-8"' );
  103. }
  104. else {
  105. $response = HTTP::Response->new( 404, 'NOT FOUND', $response->headers );
  106. }
  107. return $response;
  108. }
  109. sub get {
  110. my ( $self, $request, $response ) = @_;
  111. my $path = uri_unescape $request->uri->path;
  112. my $fs = $self->filesys;
  113. if ( $fs->test( 'f', $path ) && $fs->test( 'r', $path ) ) {
  114. my $fh = $fs->open_read($path);
  115. my $file = join '', <$fh>;
  116. $fs->close_read($fh);
  117. $response->content($file);
  118. $response->last_modified( $fs->modtime($path) );
  119. }
  120. elsif ( $fs->test( 'd', $path ) ) {
  121. # a web browser, then
  122. my @files = $fs->list($path);
  123. my $body;
  124. my $fpath = $path =~ m{/$} ? $path : $path . '/';
  125. foreach my $file (@files) {
  126. if ( $fs->test( 'd', $fpath . $file ) ) {
  127. $body .= qq|<a href="$file/">$file/</a><br>\n|;
  128. }
  129. else {
  130. $file =~ s{/$}{};
  131. $body .= qq|<a href="$file">$file</a><br>\n|;
  132. }
  133. }
  134. $response->header( 'Content-Type' => 'text/html; charset="utf-8"' );
  135. $response->content($body);
  136. }
  137. else {
  138. return HTTP::Response->new( 404, 'Not Found' );
  139. }
  140. return $response;
  141. }
  142. sub _lock_manager {
  143. my ($self) = @_;
  144. unless ( $self->{'lock_manager'} ) {
  145. if ( $self->{'_dsn'} ) {
  146. my $db = Net::DAV::LockManager::DB->new( $self->{'_dsn'} );
  147. $self->{'lock_manager'} = Net::DAV::LockManager->new($db);
  148. }
  149. else {
  150. $self->{'lock_manager'} = Net::DAV::LockManager->new();
  151. }
  152. }
  153. return $self->{'lock_manager'};
  154. }
  155. sub lock {
  156. my ( $self, $request, $response ) = @_;
  157. my $lockreq = _parse_lock_request($request);
  158. # Invalid XML requires a 400 response code.
  159. return HTTP::Response->new( 400, 'Bad Request' ) unless defined $lockreq;
  160. if ( !$lockreq->{'has_content'} ) {
  161. # Not already locked.
  162. return HTTP::Response->new( 403, 'Forbidden' ) if !$lockreq->{'token'};
  163. # Reset timeout
  164. if ( my $lock = $self->_lock_manager()->refresh_lock($lockreq) ) {
  165. $response->header( 'Content-Type' => 'text/xml; charset="utf-8"' );
  166. $response->content(
  167. _lock_response_content(
  168. {
  169. 'path' => $lock->path,
  170. 'token' => $lock->token,
  171. 'timeout' => $lock->timeout,
  172. 'scope' => $lock->scope,
  173. 'depth' => $lock->depth,
  174. }
  175. )
  176. );
  177. }
  178. else {
  179. my $curr = $self->_lock_manager()->find_lock( { 'path' => $lockreq->{'path'} } );
  180. return HTTP::Response->new( 412, 'Precondition Failed' ) unless $curr;
  181. # Not the correct lock token
  182. return HTTP::Response->new( 412, 'Precondition Failed' ) if $lockreq->{'token'} ne $curr->token;
  183. # Not the correct user.
  184. return HTTP::Response->new( 403, 'Forbidden' );
  185. }
  186. return $response;
  187. }
  188. # Validate depth request
  189. return HTTP::Response->new( 400, 'Bad Request' ) unless $lockreq->{'depth'} =~ /^(?:0|infinity)$/;
  190. my $lock = $self->_lock_manager()->lock($lockreq);
  191. if ( !$lock ) {
  192. my $curr = $self->_lock_manager()->find_lock( { 'path' => $lockreq->{'path'} } );
  193. return HTTP::Response->new( 412, 'Precondition Failed' ) unless $curr;
  194. # Not the correct lock token
  195. return HTTP::Response->new( 412, 'Precondition Failed' ) if $lockreq->{'token'}||'' ne $curr->token;
  196. # Resource is already locked
  197. return HTTP::Response->new( 403, 'Forbidden' );
  198. }
  199. my $token = $lock->token;
  200. $response->code( 200 );
  201. $response->message( 'OK' );
  202. $response->header( 'Lock-Token', "<$token>" );
  203. $response->header( 'Content-Type', 'text/xml; charset="utf-8"' );
  204. $response->content(
  205. _lock_response_content(
  206. {
  207. 'path' => $lock->path,
  208. 'token' => $token,
  209. 'timeout' => $lock->timeout,
  210. 'scope' => 'exclusive',
  211. 'depth' => $lock->depth,
  212. 'owner_node' => $lockreq->{'owner_node'},
  213. }
  214. )
  215. );
  216. # Create empty file if none exists, as per RFC 4918, Section 9.10.4
  217. my $fs = $self->filesys;
  218. if ( !$fs->test( 'e', $lock->path ) ) {
  219. my $fh = $fs->open_write( $lock->path, 1 );
  220. $fs->close_write($fh) if $fh;
  221. }
  222. return $response;
  223. }
  224. sub _get_timeout {
  225. my ($to_header) = @_;
  226. return undef unless defined $to_header and length $to_header;
  227. my @timeouts = sort
  228. map { /Second-(\d+)/ ? $1 : $_ }
  229. grep { $_ ne 'Infinite' }
  230. split /\s*,\s*/, $to_header;
  231. return undef unless @timeouts;
  232. return $timeouts[0];
  233. }
  234. sub _parse_lock_header {
  235. my ($req) = @_;
  236. my $depth = $req->header('Depth');
  237. my %lockreq = (
  238. 'path' => uri_unescape( $req->uri->path ),
  239. # Assuming basic auth for now.
  240. 'user' => ( $req->authorization_basic() )[0] || '',
  241. 'token' => ( _extract_lock_token($req) || undef ),
  242. 'timeout' => _get_timeout( $req->header('Timeout') ),
  243. 'depth' => ( defined $depth ? $depth : 'infinity' ),
  244. );
  245. return \%lockreq;
  246. }
  247. sub _parse_lock_request {
  248. my ($req) = @_;
  249. my $lockreq = _parse_lock_header($req);
  250. return $lockreq unless $req->content;
  251. my $parser = XML::LibXML->new;
  252. my $doc;
  253. eval { $doc = $parser->parse_string( $req->content ); } or do {
  254. # Request body must be a valid XML request
  255. return;
  256. };
  257. my $xpc = XML::LibXML::XPathContext->new($doc);
  258. $xpc->registerNs( 'D', 'DAV:' );
  259. # Want the following in list context.
  260. $lockreq->{'owner_node'} = ( $xpc->findnodes('/D:lockinfo/D:owner') )[0];
  261. if ( $lockreq->{'owner_node'} ) {
  262. my $owner = $lockreq->{'owner_node'}->toString;
  263. $owner =~ s/^<(?:[^:]+:)?owner>//sm;
  264. $owner =~ s!</(?:[^:]+:)?owner>$!!sm;
  265. $lockreq->{'owner'} = $owner;
  266. }
  267. $lockreq->{'scope'} = eval { ( $xpc->findnodes('/D:lockinfo/D:lockscope/D:*') )[0]->localname; };
  268. $lockreq->{'has_content'} = 1;
  269. return $lockreq;
  270. }
  271. sub _extract_lock_token {
  272. my ($req) = @_;
  273. my $token = $req->header('If');
  274. unless ($token) {
  275. $token = $req->header('Lock-Token');
  276. return $1 if defined $token && $token =~ /<([^>]+)>/;
  277. return undef;
  278. }
  279. # Based on the last paragraph of section 10.4.1 of RFC 4918, it appears
  280. # that any lock token that appears in the If header is available as a
  281. # known lock token. Rather than trying to deal with the whole entity,
  282. # lock, implicit and/or, and Not (with and without resources) thing,
  283. # This code just returns a list of lock tokens found in the header.
  284. my @tokens = map { $_ =~ /<([^>]+)>/g } ( $token =~ /\(([^\)]+)\)/g );
  285. return undef unless @tokens;
  286. return @tokens == 1 ? $tokens[0] : \@tokens;
  287. }
  288. sub _lock_response_content {
  289. my ($args) = @_;
  290. my $resp = XML::LibXML::Document->new( '1.0', 'utf-8' );
  291. my $prop = _dav_root( $resp, 'prop' );
  292. my $lock = _dav_child( _dav_child( $prop, 'lockdiscovery' ), 'activelock' );
  293. _dav_child( _dav_child( $lock, 'locktype' ), 'write' );
  294. _dav_child( _dav_child( $lock, 'lockscope' ), $args->{'scope'} || 'exclusive' );
  295. _dav_child( $lock, 'depth', $args->{'depth'} || 'infinity' );
  296. if ( $args->{'owner_node'} ) {
  297. my $owner = $args->{'owner_node'}->cloneNode(1);
  298. $resp->adoptNode($owner);
  299. $lock->addChild($owner);
  300. }
  301. _dav_child( $lock, 'timeout', "Second-$args->{'timeout'}" );
  302. _dav_child( _dav_child( $lock, 'locktoken' ), 'href', $args->{'token'} );
  303. _dav_child( _dav_child( $lock, 'lockroot' ), 'href', $args->{'path'} );
  304. return $resp->toString;
  305. }
  306. sub _active_lock_prop {
  307. my ( $doc, $lock ) = @_;
  308. my $active = $doc->createElement('D:activelock');
  309. # All locks are write
  310. _dav_child( _dav_child( $active, 'locktype' ), 'write' );
  311. _dav_child( _dav_child( $active, 'lockscope' ), $lock->scope );
  312. _dav_child( $active, 'depth', $lock->depth );
  313. $active->appendWellBalancedChunk( '<D:owner xmlns:D="DAV:">' . $lock->owner . '</D:owner>' );
  314. _dav_child( $active, 'timeout', 'Second-' . $lock->timeout );
  315. _dav_child( _dav_child( $active, 'locktoken' ), 'href', $lock->token );
  316. _dav_child( _dav_child( $active, 'lockroot' ), 'href', $lock->path );
  317. return $active;
  318. }
  319. sub unlock {
  320. my ( $self, $request, $response ) = @_;
  321. my $path = uri_unescape( $request->uri->path );
  322. my $lockreq = _parse_lock_header($request);
  323. # No lock token supplied, we cannot unlock
  324. return HTTP::Response->new( 400, 'Bad Request' ) unless $lockreq->{'token'};
  325. if ( !$self->_lock_manager()->unlock($lockreq) ) {
  326. my $curr = $self->_lock_manager()->find_lock( { 'path' => $lockreq->{'path'} } );
  327. # No lock exists, conflicting requirements.
  328. return HTTP::Response->new( 409, 'Conflict' ) unless $curr;
  329. # Not the owner of the lock or bad token.
  330. return HTTP::Response->new( 403, 'Forbidden' );
  331. }
  332. return HTTP::Response->new( 204, 'No content' );
  333. }
  334. sub _dav_child {
  335. my ( $parent, $tag, $text ) = @_;
  336. my $child = $parent->ownerDocument->createElement("D:$tag");
  337. $parent->addChild($child);
  338. $child->appendText($text) if defined $text;
  339. return $child;
  340. }
  341. sub _dav_root {
  342. my ( $doc, $tag ) = @_;
  343. my $root = $doc->createElementNS( 'DAV:', $tag );
  344. $root->setNamespace( 'DAV:', 'D', 1 );
  345. $doc->setDocumentElement($root);
  346. return $root;
  347. }
  348. sub _can_modify {
  349. my ( $self, $request ) = @_;
  350. my $lockreq = _parse_lock_header($request);
  351. return $self->_lock_manager()->can_modify($lockreq);
  352. }
  353. sub post {
  354. my ( $self, $request, $response ) = @_;
  355. if ( !$self->_can_modify( $request ) ) {
  356. return HTTP::Response->new( 403, 'Forbidden' );
  357. }
  358. return HTTP::Response->new( 501, 'Not Implemented' );
  359. }
  360. sub put {
  361. my ( $self, $request, $response ) = @_;
  362. if ( !$self->_can_modify($request) ) {
  363. return HTTP::Response->new( 403, 'Forbidden' );
  364. }
  365. my $path = uri_unescape $request->uri->path;
  366. my $fs = $self->filesys;
  367. return HTTP::Response->new( 405, 'Method Not Allowed' ) if $fs->test( 'd', $path );
  368. my $parent = $path;
  369. $parent =~ s{/[^/]+$}{};
  370. $parent = '/' if $parent eq '';
  371. # Parent directory does not exist.
  372. return HTTP::Response->new( 409, 'Conflict' ) unless $fs->test( 'd', $parent );
  373. my $fh = $fs->open_write( $path );
  374. if ( $fh ) {
  375. $response = HTTP::Response->new( 201, 'Created', $response->headers );
  376. print $fh $request->content;
  377. $fs->close_write($fh);
  378. }
  379. else {
  380. # Unable to write for some other reason.
  381. return HTTP::Response->new( 403, 'Forbidden' );
  382. }
  383. return $response;
  384. }
  385. sub _delete_xml {
  386. my ( $dom, $path ) = @_;
  387. my $response = $dom->createElement('d:response');
  388. $response->appendTextChild( 'd:href' => $path );
  389. $response->appendTextChild( 'd:status' => 'HTTP/1.1 401 Permission Denied' ); # *** FIXME ***
  390. }
  391. sub delete {
  392. my ( $self, $request, $response ) = @_;
  393. if ( !$self->_can_modify($request) ) {
  394. return HTTP::Response->new( 403, 'Forbidden' );
  395. }
  396. if ( $request->uri->fragment ) {
  397. return HTTP::Response->new( 404, 'Not Found', $response->headers );
  398. }
  399. my $path = uri_unescape $request->uri->path;
  400. my $fs = $self->filesys;
  401. unless ( $fs->test( 'e', $path ) ) {
  402. return HTTP::Response->new( 404, 'Not Found', $response->headers );
  403. }
  404. my $dom = XML::LibXML::Document->new( '1.0', 'utf-8' );
  405. my @error;
  406. # see rt 46865: files first since rmdir() only removed empty directories
  407. foreach my $part ( _get_files($fs, $path), _get_dirs($fs, $path), $path ) {
  408. next unless $fs->test( 'e', $part );
  409. if ( $fs->test( 'f', $part ) ) {
  410. push @error, _delete_xml( $dom, $part )
  411. unless $fs->delete($part);
  412. }
  413. elsif ( $fs->test( 'd', $part ) ) {
  414. push @error, _delete_xml( $dom, $part )
  415. unless $fs->rmdir($part);
  416. }
  417. }
  418. if (@error) {
  419. my $multistatus = $dom->createElement('D:multistatus');
  420. $multistatus->setAttribute( 'xmlns:D', 'DAV:' );
  421. $multistatus->addChild($_) foreach @error;
  422. $response = HTTP::Response->new( 207 => 'Multi-Status' );
  423. $response->header( 'Content-Type' => 'text/xml; charset="utf-8"' );
  424. }
  425. else {
  426. $response = HTTP::Response->new( 204 => 'No Content' );
  427. }
  428. return $response;
  429. }
  430. sub copy {
  431. my ( $self, $request, $response ) = @_;
  432. my $path = uri_unescape $request->uri->path;
  433. $path =~ s{/+$}{}; # see rt 46865
  434. # need to modify request to pay attention to destination address.
  435. my $lockreq = _parse_lock_header( $request );
  436. $lockreq->{'path'} = uri_unescape( $request->header( 'Destination' ) );
  437. if ( !$self->_lock_manager()->can_modify( $lockreq ) ) {
  438. return HTTP::Response->new( 403, 'Forbidden' );
  439. }
  440. my $fs = $self->filesys;
  441. my $destination = $request->header('Destination');
  442. $destination = URI->new($destination)->path;
  443. $destination =~ s{/+$}{}; # see rt 46865
  444. my $depth = $request->header('Depth');
  445. $depth = '' if !defined $depth;
  446. my $overwrite = $request->header('Overwrite') || 'F';
  447. if ( $fs->test( "f", $path ) ) {
  448. return $self->_copy_file( $request, $response );
  449. }
  450. my @files = _get_files($fs, $path, $depth);
  451. my @dirs = _get_dirs($fs, $path, $depth);
  452. push @dirs, $path;
  453. foreach my $dir ( sort @dirs ) {
  454. my $destdir = $dir;
  455. $destdir =~ s/^$path/$destination/;
  456. if ( $overwrite eq 'F' && $fs->test( "e", $destdir ) ) {
  457. return HTTP::Response->new( 401, "ERROR", $response->headers );
  458. }
  459. $fs->mkdir($destdir);
  460. }
  461. foreach my $file ( reverse sort @files ) {
  462. my $destfile = $file;
  463. $destfile =~ s/^$path/$destination/;
  464. my $fh = $fs->open_read($file);
  465. my $file = join '', <$fh>;
  466. $fs->close_read($fh);
  467. if ( $fs->test( 'e', $destfile ) ) {
  468. if ( $overwrite eq 'T' ) {
  469. $fh = $fs->open_write($destfile);
  470. print $fh $file;
  471. $fs->close_write($fh);
  472. }
  473. else {
  474. return HTTP::Response( 412, 'Precondition Failed' );
  475. }
  476. }
  477. else {
  478. $fh = $fs->open_write($destfile);
  479. print $fh $file;
  480. $fs->close_write($fh);
  481. }
  482. }
  483. $response = HTTP::Response->new( 200, 'OK', $response->headers );
  484. return $response;
  485. }
  486. sub _copy_file {
  487. my ( $self, $request, $response ) = @_;
  488. my $path = uri_unescape $request->uri->path;
  489. my $fs = $self->filesys;
  490. my $destination = $request->header('Destination');
  491. $destination = URI->new($destination)->path;
  492. my $depth = $request->header('Depth');
  493. my $overwrite = $request->header('Overwrite');
  494. if ( $fs->test( 'd', $destination ) ) {
  495. return HTTP::Response->new( 204, 'No Content', $response->headers );
  496. }
  497. if ( $fs->test( 'f', $path ) && $fs->test( 'r', $path ) ) {
  498. my $fh = $fs->open_read($path);
  499. my $file = join '', <$fh>;
  500. $fs->close_read($fh);
  501. if ( $fs->test( 'f', $destination ) ) {
  502. if ( $overwrite eq 'T' ) {
  503. $fh = $fs->open_write($destination);
  504. print $fh $file;
  505. $fs->close_write($fh);
  506. }
  507. else {
  508. return HTTP::Response( 412, 'Precondition Failed' );
  509. }
  510. }
  511. else {
  512. unless ( $fh = $fs->open_write($destination) ) {
  513. return HTTP::Response->new( 409, 'Conflict' );
  514. }
  515. print $fh $file;
  516. $fs->close_write($fh);
  517. $response->code(201);
  518. $response->message('Created');
  519. }
  520. }
  521. else {
  522. return HTTP::Response->new( 404, 'Not Found' );
  523. }
  524. return $response;
  525. }
  526. sub move {
  527. my ( $self, $request, $response ) = @_;
  528. # need to check both paths for locks.
  529. my $lockreq = _parse_lock_header( $request );
  530. if ( !$self->_lock_manager()->can_modify( $lockreq ) ) {
  531. return HTTP::Response->new( 403, 'Forbidden' );
  532. }
  533. $lockreq->{'path'} = uri_unescape( $request->header( 'Destination' ) );
  534. if ( !$self->_lock_manager()->can_modify( $lockreq ) ) {
  535. return HTTP::Response->new( 403, 'Forbidden' );
  536. }
  537. my $destination = $request->header('Destination');
  538. $destination = URI->new($destination)->path;
  539. my $destexists = $self->filesys->test( "e", $destination );
  540. $response = $self->copy( $request, $response );
  541. $response = $self->delete( $request, $response )
  542. if $response->is_success;
  543. $response->code(201) unless $destexists;
  544. return $response;
  545. }
  546. sub mkcol {
  547. my ( $self, $request, $response ) = @_;
  548. my $path = uri_unescape $request->uri->path;
  549. if ( !$self->_can_modify($request) ) {
  550. return HTTP::Response->new( 403, 'Forbidden' );
  551. }
  552. my $fs = $self->filesys;
  553. return HTTP::Response->new( 415, 'Unsupported Media Type' ) if $request->content;
  554. return HTTP::Response->new( 405, 'Method Not Allowed' ) if $fs->test( 'e', $path );
  555. $fs->mkdir($path);
  556. if ( $fs->test( 'd', $path ) ) {
  557. $response->code(201);
  558. $response->message('Created');
  559. }
  560. else {
  561. $response->code(409);
  562. $response->message('Conflict');
  563. }
  564. return $response;
  565. }
  566. sub propfind {
  567. my ( $self, $request, $response ) = @_;
  568. my $path = uri_unescape $request->uri->path;
  569. my $fs = $self->filesys;
  570. my $depth = $request->header('Depth');
  571. my $reqinfo = 'allprop';
  572. my @reqprops;
  573. if ( $request->header('Content-Length') ) {
  574. my $content = $request->content;
  575. my $parser = XML::LibXML->new;
  576. my $doc;
  577. eval { $doc = $parser->parse_string($content); };
  578. if ($@) {
  579. return HTTP::Response->new( 400, 'Bad Request' );
  580. }
  581. #$reqinfo = doc->find('/DAV:propfind/*')->localname;
  582. $reqinfo = $doc->find('/*/*')->shift->localname;
  583. if ( $reqinfo eq 'prop' ) {
  584. #for my $node ($doc->find('/DAV:propfind/DAV:prop/*')) {
  585. for my $node ( $doc->find('/*/*/*')->get_nodelist ) {
  586. push @reqprops, [ $node->namespaceURI, $node->localname ];
  587. }
  588. }
  589. }
  590. if ( !$fs->test( 'e', $path ) ) {
  591. return HTTP::Response->new( 404, 'Not Found' );
  592. }
  593. $response->code(207);
  594. $response->message('Multi-Status');
  595. $response->header( 'Content-Type' => 'text/xml; charset="utf-8"' );
  596. my $doc = XML::LibXML::Document->new( '1.0', 'utf-8' );
  597. my $multistat = $doc->createElement('D:multistatus');
  598. $multistat->setAttribute( 'xmlns:D', 'DAV:' );
  599. $doc->setDocumentElement($multistat);
  600. my @paths;
  601. if ( defined $depth && $depth eq 1 and $fs->test( 'd', $path ) ) {
  602. my $p = $path;
  603. $p .= '/' unless $p =~ m{/$};
  604. @paths = map { $p . $_ } File::Spec->no_upwards( $fs->list($path) );
  605. push @paths, $path;
  606. }
  607. else {
  608. @paths = ($path);
  609. }
  610. for my $path (@paths) {
  611. my (
  612. $dev, $ino, $mode, $nlink, $uid, $gid, $rdev,
  613. $size, $atime, $mtime, $ctime, $blksize, $blocks
  614. ) = $fs->stat($path);
  615. # modified time is stringified human readable HTTP::Date style
  616. $mtime = time2str($mtime);
  617. # created time is ISO format
  618. # tidy up date format - isoz isn't exactly what we want, but
  619. # it's easy to change.
  620. $ctime = time2isoz($ctime);
  621. $ctime =~ s/ /T/;
  622. $ctime =~ s/Z//;
  623. $size ||= '';
  624. my $is_dir = $fs->test( 'd', $path );
  625. my $resp = _dav_child( $multistat, 'response' );
  626. my $href = File::Spec->catdir(
  627. map { uri_escape $_} File::Spec->splitdir($path)
  628. ) . ( $is_dir && $path !~ m{/$} ? '/' : '');
  629. $href =~ tr{\\}{/}; # Protection from wrong slashes under Windows.
  630. _dav_child( $resp, 'href', $href );
  631. my $okprops = $doc->createElement('D:prop');
  632. my $nfprops = $doc->createElement('D:prop');
  633. my $prop;
  634. if ( $reqinfo eq 'prop' ) {
  635. my %prefixes = ( 'DAV:' => 'D' );
  636. my $i = 0;
  637. for my $reqprop (@reqprops) {
  638. my ( $ns, $name ) = @$reqprop;
  639. if ( $ns eq 'DAV:' && $name eq 'creationdate' ) {
  640. _dav_child( $okprops, 'creationdate', $ctime );
  641. }
  642. elsif ( $ns eq 'DAV:' && $name eq 'getcontentlength' ) {
  643. _dav_child( $okprops, 'getcontentlength', $is_dir ? () : ($size) );
  644. }
  645. elsif ( $ns eq 'DAV:' && $name eq 'getcontenttype' ) {
  646. _dav_child( $okprops, 'getcontenttype', $is_dir ? 'httpd/unix-directory' : 'httpd/unix-file' );
  647. }
  648. elsif ( $ns eq 'DAV:' && $name eq 'getlastmodified' ) {
  649. _dav_child( $okprops, 'getlastmodified', $mtime );
  650. }
  651. elsif ( $ns eq 'DAV:' && $name eq 'resourcetype' ) {
  652. $prop = _dav_child( $okprops, 'resourcetype' );
  653. if ( $is_dir ) {
  654. _dav_child( $prop, 'collection' );
  655. }
  656. }
  657. elsif ( $ns eq 'DAV:' && $name eq 'lockdiscovery' ) {
  658. $prop = _dav_child( $okprops, 'lockdiscovery' );
  659. my $user = ($request->authorization_basic())[0]||'';
  660. foreach my $lock ( $self->_lock_manager()->list_all_locks({ 'path' => $path, 'user' => $user }) ) {
  661. my $active = _active_lock_prop( $doc, $lock );
  662. $prop->addChild( $active );
  663. }
  664. }
  665. elsif ( $ns eq 'DAV:' && $name eq 'supportedlock' ) {
  666. $prop = _supportedlock_child( $okprops );
  667. }
  668. else {
  669. my $prefix = $prefixes{$ns};
  670. if ( !defined $prefix ) {
  671. $prefix = 'i' . $i++;
  672. # mod_dav sets <response> 'xmlns' attribute - whatever
  673. #$nfprops->setAttribute("xmlns:$prefix", $ns);
  674. $resp->setAttribute( "xmlns:$prefix", $ns );
  675. $prefixes{$ns} = $prefix;
  676. }
  677. $prop = $doc->createElement("$prefix:$name");
  678. $nfprops->addChild($prop);
  679. }
  680. }
  681. }
  682. elsif ( $reqinfo eq 'propname' ) {
  683. _dav_child( $okprops, 'creationdate' );
  684. _dav_child( $okprops, 'getcontentlength' );
  685. _dav_child( $okprops, 'getcontenttype' );
  686. _dav_child( $okprops, 'getlastmodified' );
  687. _dav_child( $okprops, 'supportedlock' );
  688. _dav_child( $okprops, 'resourcetype' );
  689. }
  690. else {
  691. _dav_child( $okprops, 'creationdate', $ctime );
  692. _dav_child( $okprops, 'getcontentlength', $is_dir ? () : ($size) );
  693. _dav_child( $okprops, 'getcontenttype', $is_dir ? 'httpd/unix-directory' : 'httpd/unix-file' );
  694. _dav_child( $okprops, 'getlastmodified', $mtime );
  695. $prop = _supportedlock_child( $okprops );
  696. my $user = ($request->authorization_basic())[0]||'';
  697. my @locks = $self->_lock_manager()->list_all_locks({ 'path' => $path, 'user' => $user });
  698. if ( @locks ) {
  699. $prop = _dav_child( $okprops, 'lockdiscovery' );
  700. foreach my $lock ( @locks ) {
  701. my $active = _active_lock_prop( $doc, $lock );
  702. $prop->addChild( $active );
  703. }
  704. }
  705. $prop = _dav_child( $okprops, 'resourcetype' );
  706. if ( $is_dir ) {
  707. _dav_child( $prop, 'collection' );
  708. }
  709. }
  710. if ( $okprops->hasChildNodes ) {
  711. my $propstat = _dav_child( $resp, 'propstat' );
  712. $propstat->addChild($okprops);
  713. _dav_child( $propstat, 'status', 'HTTP/1.1 200 OK' );
  714. }
  715. if ( $nfprops->hasChildNodes ) {
  716. my $propstat = _dav_child( $resp, 'propstat' );
  717. $propstat->addChild($nfprops);
  718. _dav_child( $propstat, 'status', 'HTTP/1.1 404 Not Found' );
  719. }
  720. }
  721. #this must be 0 as certin ms webdav clients choke on 1
  722. $response->content( $doc->toString(0) );
  723. return $response;
  724. }
  725. sub _supportedlock_child {
  726. my ($okprops) = @_;
  727. my $prop = _dav_child( $okprops, 'supportedlock' );
  728. #for my $n (qw(exclusive shared)) { # shared is currently not supported.
  729. for my $n (qw(exclusive)) {
  730. my $lock = _dav_child( $prop, 'lockentry' );
  731. _dav_child( _dav_child( $lock, 'lockscope' ), $n );
  732. _dav_child( _dav_child( $lock, 'locktype' ), 'write' );
  733. }
  734. return $prop;
  735. }
  736. sub _get_files {
  737. my ($fs, $path, $depth) = @_;
  738. reverse map { s{/+}{/}g;s{/$}{}; $_ }
  739. (defined $depth && $depth =~ m{\A\d+\z}) ?
  740. File::Find::Rule::Filesys::Virtual->virtual($fs)->file->maxdepth($depth)->in($path)
  741. : File::Find::Rule::Filesys::Virtual->virtual($fs)->file->in($path)
  742. ;
  743. }
  744. sub _get_dirs {
  745. my ($fs, $path, $depth) = @_;
  746. return reverse sort
  747. grep { $_ !~ m{/\.\.?$} }
  748. map { s{/+}{/}g;s{/$}{}; $_ }
  749. (defined $depth && $depth =~ m{\A\d+\z}) ?
  750. File::Find::Rule::Filesys::Virtual->virtual($fs)->directory->maxdepth($depth)->in($path)
  751. : File::Find::Rule::Filesys::Virtual->virtual($fs)->directory->in($path)
  752. ;
  753. }
  754. 1;
  755. __END__
  756. =head1 NAME
  757. Net::DAV::Server - Provide a DAV Server
  758. =head1 SYNOPSIS
  759. my $filesys = Filesys::Virtual::Plain->new({root_path => $cwd});
  760. my $webdav = Net::DAV::Server->new();
  761. $webdav->filesys($filesys);
  762. my $d = HTTP::Daemon->new(
  763. LocalAddr => 'localhost',
  764. LocalPort => 4242,
  765. ReuseAddr => 1) || die;
  766. print "Please contact me at: ", $d->url, "\n";
  767. while (my $c = $d->accept) {
  768. while (my $request = $c->get_request) {
  769. my $response = $webdav->run($request);
  770. $c->send_response ($response);
  771. }
  772. $c->close;
  773. undef($c);
  774. }
  775. =head1 DESCRIPTION
  776. This module provides a WebDAV server. WebDAV stands for "Web-based
  777. Distributed Authoring and Versioning". It is a set of extensions to
  778. the HTTP protocol which allows users to collaboratively edit and
  779. manage files on remote web servers.
  780. Net::DAV::Server provides a WebDAV server and exports a filesystem for
  781. you using the Filesys::Virtual suite of modules. If you simply want to
  782. export a local filesystem, use Filesys::Virtual::Plain as above.
  783. This module doesn't currently provide a full WebDAV
  784. implementation. However, I am working through the WebDAV server
  785. protocol compliance test suite (litmus, see
  786. http://www.webdav.org/neon/litmus/) and will provide more compliance
  787. in future. The important thing is that it supports cadaver and the Mac
  788. OS X Finder as clients.
  789. =head1 AUTHOR
  790. Leon Brocard <acme@astray.com>
  791. =head1 MAINTAINERS
  792. G. Wade Johnson <wade@cpanel.net> ( co-maintainer )
  793. Erin Schoenhals <erin@cpanel.net> ( co-maintainer )
  794. Bron Gondwana <perlcode@brong.net> ( co-maintainer )
  795. Leon Brocard <acme@astray.com> ( original author )
  796. The latest copy of this package can be checked out using Subversion
  797. from http://svn.brong.net/netdavserver/release
  798. Development code at http://svn.brong.net/netdavserver/trunk
  799. =head1 COPYRIGHT
  800. Copyright (C) 2004, Leon Brocard
  801. Changes copyright (c) 2010, cPanel, Inc.
  802. This module is free software; you can redistribute it or modify it under
  803. the same terms as Perl itself.
  804. =cut
  805. 1