UNPKG

jsdav-ext

Version:

jsDAV allows you to easily add WebDAV support to a NodeJS application. jsDAV is meant to cover the entire standard, and attempts to allow integration using an easy to understand API.

1,289 lines (1,168 loc) 106 kB
/* * @package jsDAV * @subpackage DAV * @copyright Copyright(c) 2011 Ajax.org B.V. <info AT ajax DOT org> * @author Mike de Boer <info AT mikedeboer DOT nl> * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License */ "use strict"; // DAV classes used directly by the Handler object var jsDAV = require("./../jsdav"); var jsDAV_Server = require("./server"); var jsDAV_Property_Response = require("./property/response"); var jsDAV_Property_GetLastModified = require("./property/getLastModified"); var jsDAV_Property_ResourceType = require("./property/resourceType"); var jsDAV_Property_SupportedReportSet = require("./property/supportedReportSet"); // interfaces to check for: var jsDAV_iFile = require("./interfaces/iFile"); var jsDAV_iCollection = require("./interfaces/iCollection"); var jsDAV_iExtendedCollection = require("./interfaces/iExtendedCollection") var jsDAV_iQuota = require("./interfaces/iQuota"); var jsDAV_iProperties = require("./interfaces/iProperties"); var Url = require("url"); var Fs = require("fs"); var Path = require("path"); var AsyncEventEmitter = require("./../shared/asyncEvents").EventEmitter; var Exc = require("./../shared/exceptions"); var Util = require("./../shared/util"); var Xml = require("./../shared/xml"); var Async = require("asyncjs"); var Formidable = require("formidable"); var requestCounter = 0; /** * Called when an http request comes in, pass it on to invoke and handle any * exceptions that might be thrown * * @param {jsDav_Server} server * @param {ServerRequest} req * @param {ServerResponse} resp * @return {jsDAV_Handler} */ var jsDAV_Handler = module.exports = function(server, req, resp) { this.server = server; this.httpRequest = Util.streamBuffer(req); this.httpResponse = resp; this.plugins = {}; this.nodeCache = {}; for (var plugin in server.plugins) { if (typeof server.plugins[plugin] != "object") continue; this.plugins[plugin] = server.plugins[plugin].new(this); } try { this.invoke(); } catch (ex) { this.handleError(ex); } }; /** * Inifinity is used for some request supporting the HTTP Depth header and indicates * that the operation should traverse the entire tree */ jsDAV_Handler.DEPTH_INFINITY = -1; /** * Nodes that are files, should have this as the type property */ jsDAV_Handler.NODE_FILE = 1; /** * Nodes that are directories, should use this value as the type property */ jsDAV_Handler.NODE_DIRECTORY = 2; jsDAV_Handler.PROP_SET = 1; jsDAV_Handler.PROP_REMOVE = 2; jsDAV_Handler.STATUS_MAP = { "100": "Continue", "101": "Switching Protocols", "200": "OK", "201": "Created", "202": "Accepted", "203": "Non-Authorative Information", "204": "No Content", "205": "Reset Content", "206": "Partial Content", "207": "Multi-Status", // RFC 4918 "208": "Already Reported", // RFC 5842 "300": "Multiple Choices", "301": "Moved Permanently", "302": "Found", "303": "See Other", "304": "Not Modified", "305": "Use Proxy", "307": "Temporary Redirect", "400": "Bad request", "401": "Unauthorized", "402": "Payment Required", "403": "Forbidden", "404": "Not Found", "405": "Method Not Allowed", "406": "Not Acceptable", "407": "Proxy Authentication Required", "408": "Request Timeout", "409": "Conflict", "410": "Gone", "411": "Length Required", "412": "Precondition failed", "413": "Request Entity Too Large", "414": "Request-URI Too Long", "415": "Unsupported Media Type", "416": "Requested Range Not Satisfiable", "417": "Expectation Failed", "418": "I'm a teapot", // RFC 2324 "422": "Unprocessable Entity", // RFC 4918 "423": "Locked", // RFC 4918 "424": "Failed Dependency", // RFC 4918 "500": "Internal Server Error", "501": "Not Implemented", "502": "Bad Gateway", "503": "Service Unavailable", "504": "Gateway Timeout", "505": "HTTP Version not supported", "507": "Unsufficient Storage", // RFC 4918 "508": "Loop Detected" // RFC 5842 }; (function() { /** * httpResponse * * @var HTTP_Response */ this.httpResponse = /** * httpRequest * * @var HTTP_Request */ this.httpRequest = null; /** * The propertymap can be used to map properties from * requests to property classes. * * @var array */ this.propertyMap = { "{DAV:}resourcetype": jsDAV_Property_ResourceType }; this.protectedProperties = [ // RFC4918 "{DAV:}getcontentlength", "{DAV:}getetag", "{DAV:}getlastmodified", "{DAV:}lockdiscovery", "{DAV:}resourcetype", "{DAV:}supportedlock", // RFC4331 "{DAV:}quota-available-bytes", "{DAV:}quota-used-bytes", // RFC3744 "{DAV:}alternate-URI-set", "{DAV:}principal-URL", "{DAV:}group-membership", "{DAV:}supported-privilege-set", "{DAV:}current-user-privilege-set", "{DAV:}acl", "{DAV:}acl-restrictions", "{DAV:}inherited-acl-set", "{DAV:}principal-collection-set", // RFC5397 "{DAV:}current-user-principal" ]; /** * This property allows you to automatically add the 'resourcetype' value * based on a node's classname or interface. * * The preset ensures that {DAV:}collection is automaticlly added for nodes * implementing jsDAV_iCollection. * * @var object */ this.resourceTypeMapping = { "{DAV:}collection": jsDAV_iCollection }; var internalMethods = { "OPTIONS":1, "GET":1, "HEAD":1, "DELETE":1, "PROPFIND":1, "MKCOL":1, "PUT":1, "PROPPATCH":1, "COPY":1, "MOVE":1, "REPORT":1 }; /** * Handles a http request, and execute a method based on its name * * @return void */ this.invoke = function() { var method = this.httpRequest.method.toUpperCase(), self = this; if (jsDAV.debugMode) { this.id = ++requestCounter; Util.log("{" + this.id + "}", method, this.httpRequest.url); Util.log("{" + this.id + "}", this.httpRequest.headers); var wh = this.httpResponse.writeHead, we = this.httpResponse.end; this.httpResponse.writeHead = function(code, headers) { Util.log("{" + self.id + "}", code, headers); this.writeHead = wh; this.writeHead(code, headers); }; this.httpResponse.end = function(content) { Util.log("{" + self.id + "}", "'" + (content || "") + "'"); this.end = we; this.end(content); }; } var uri = this.getRequestUri(); this.dispatchEvent("beforeMethod", method, uri, function(stop) { if (stop === true) return; if (stop) return self.handleError(stop); // Make sure this is a HTTP method we support if (internalMethods[method]) { self["http" + method.charAt(0) + method.toLowerCase().substr(1)](); } else { self.dispatchEvent("unknownMethod", method, uri, function(stop) { if (stop === true) return; // Unsupported method self.handleError(stop || new Exc.NotImplemented()); }); } }); }; /** * Centralized error and exception handler, which constructs a proper WebDAV * 500 server error, or different depending on the error object implementation * and/ or extensions. * * @param {Error} e Error string or Exception object * @return {void} */ this.handleError = function(e) { //if (jsDAV.debugMode) // console.trace(); if (e === true) return; // plugins should return TRUE to prevent error reporting. if (typeof e == "string") e = new Exc.jsDAV_Exception(e); var xml = '<?xml version="1.0" encoding="utf-8"?>\n' + '<d:error xmlns:d="DAV:" xmlns:a="' + Xml.NS_AJAXORG + '">\n' + ' <a:exception>' + (e.type || e.toString()) + '</a:exception>\n' + ' <a:message>' + e.message + '</a:message>\n'; if (this.server.debugExceptions) { xml += '<a:file>' + (e.filename || "") + '</a:file>\n' + '<a:line>' + (e.line || "") + '</a:line>\n'; } xml += '<a:jsdav-version>' + jsDAV_Server.VERSION + '</a:jsdav-version>\n'; var code = 500; var self = this; if (e instanceof Exc.jsDAV_Exception) { code = e.code; xml = e.serialize(this, xml); e.getHTTPHeaders(this, function(err, h) { afterHeaders(h); }); } else { afterHeaders({}); } function afterHeaders(headers) { headers["Content-Type"] = "application/xml; charset=utf-8"; self.httpResponse.writeHead(code, headers); self.httpResponse.end(xml + '</d:error>', "utf-8"); if (jsDAV.debugMode) { Util.log(e, "error"); console.log(e.stack); //throw e; // DEBUGGING! } } }; /** * Caching version of jsDAV_Server#tree#getNodeForPath(), to make node lookups * during the same request (the scope of a handler instance) more cheap. * * @param {String} path * @param {Function} cbgetnodefp * @returns {void} */ this.getNodeForPath = function(path, cbgetnodefp) { if (this.nodeCache[path]) return cbgetnodefp(null, this.nodeCache[path]); var self = this; this.server.tree.getNodeForPath(path, function(err, node) { if (err) return cbgetnodefp(err); self.nodeCache[path] = node; cbgetnodefp(null, node); }); }; /** * This method is called with every tree update * * Examples of tree updates are: * * node deletions * * node creations * * copy * * move * * renaming nodes * * If Tree classes implement a form of caching, this will allow * them to make sure caches will be expired. * * If a path is passed, it is assumed that the entire subtree is dirty * * @param {String} path * @return void */ this.markDirty = function(path) { // We don't care enough about sub-paths // flushing the entire cache path = Util.trim(path, "/"); for (var nodePath in this.nodeCache) { if (nodePath.indexOf(path) === 0) delete this.nodeCache[nodePath]; } }; /** * HTTP OPTIONS * * @return {void} * @throws {Error} */ this.httpOptions = function() { var uri = this.getRequestUri(); var self = this; this.getAllowedMethods(uri, function(err, methods) { if (!Util.empty(err)) return self.handleError(err); var headers = { "Allow": methods.join(",").toUpperCase(), "MS-Author-Via" : "DAV", "Accept-Ranges" : "bytes", "X-jsDAV-Version" : jsDAV_Server.VERSION, "Content-Length" : 0 }; var features = ["1", "3", "extended-mkcol"]; for (var plugin in self.plugins) { if (!self.plugins[plugin].getFeatures) Util.log("method getFeatures() NOT implemented for plugin " + plugin, "error"); else features = features.concat(self.plugins[plugin].getFeatures()); } headers["DAV"] = features.join(","); self.httpResponse.writeHead(200, headers); self.httpResponse.end(); }); }; /** * HTTP GET * * This method simply fetches the contents of a uri, like normal * * @return {void} * @throws {Error} */ this.httpGet = function() { var node; var uri = this.getRequestUri(); var self = this; this.checkPreconditions(true, function(err, redirected) { if (!Util.empty(err)) return self.handleError(err); if (redirected) return; self.getNodeForPath(uri, function(err, n) { if (!Util.empty(err)) return self.handleError(err); node = n; afterCheck(); }); }); function afterCheck() { if (!node.hasFeature(jsDAV_iFile)) { return self.handleError(new Exc.NotImplemented( "GET is only implemented on File objects")); } var hasStream = !!node.getStream; self.getHTTPHeaders(uri, function(err, httpHeaders) { if (!Util.empty(err)) return self.handleError(err); var nodeSize = null; // ContentType needs to get a default, because many webservers // will otherwise default to text/html, and we don't want this // for security reasons. if (!httpHeaders["content-type"]) httpHeaders["content-type"] = "application/octet-stream"; if (httpHeaders["content-length"]) { nodeSize = httpHeaders["content-length"]; // Need to unset Content-Length, because we'll handle that // during figuring out the range delete httpHeaders["content-length"]; } var range = self.getHTTPRange(); var ifRange = self.httpRequest.headers["if-range"]; var ignoreRangeHeader = false; // If ifRange is set, and range is specified, we first need // to check the precondition. if (nodeSize && range && ifRange) { // if IfRange is parsable as a date we'll treat it as a // DateTime otherwise, we must treat it as an etag. try { var ifRangeDate = new Date(ifRange); // It's a date. We must check if the entity is modified // since the specified date. if (!httpHeaders["last-modified"]) { ignoreRangeHeader = true; } else { var modified = new Date(httpHeaders["last-modified"]); if (modified > ifRangeDate) ignoreRangeHeader = true; } } catch (ex) { // It's an entity. We can do a simple comparison. if (!httpHeaders["etag"]) ignoreRangeHeader = true; else if (httpHeaders["etag"] !== ifRange) ignoreRangeHeader = true; } } // We're only going to support HTTP ranges if the backend // provided a filesize if (!ignoreRangeHeader && nodeSize && range) { // Determining the exact byte offsets var start, end; if (range[0] !== null) { start = range[0]; // the browser/ client sends 'end' offsets as factor of nodeSize - 1, // so we need to correct it, because NodeJS streams byte offsets // are inclusive end = range[1] !== null ? range[1] + 1 : nodeSize; if (start > nodeSize) { return self.handleError(new Exc.RequestedRangeNotSatisfiable( "The start offset (" + range[0] + ") exceeded the size of the entity (" + nodeSize + ")") ); } if (end < start) { return self.handleError(new Exc.RequestedRangeNotSatisfiable( "The end offset (" + range[1] + ") is lower than the start offset (" + range[0] + ")") ); } if (end > nodeSize) end = nodeSize; } else { start = nodeSize - range[1]; end = nodeSize; if (start < 0) start = 0; } var offlen = end - start; // Prevent buffer error // https://github.com/joyent/node/blob/v0.4.5/lib/buffer.js#L337 if (end < start) { var swapTmp = end; end = start; start = swapTmp; } // report a different end offset, corrected by 1: var clientEnd = end > 0 ? end - 1 : end; httpHeaders["content-length"] = offlen; httpHeaders["content-range"] = "bytes " + start + "-" + clientEnd + "/" + nodeSize; if (hasStream) { var writeStreamingHeader = function () { self.httpResponse.writeHead(206, httpHeaders); }; node.getStream(start, end, function(err, data) { if (err) { if (!writeStreamingHeader) { self.httpResponse.end(); console.error("jsDAV GET error", err); } else { self.handleError(err); } return; } // write header on first incoming buffer if (writeStreamingHeader) { writeStreamingHeader(); writeStreamingHeader = null; } if (!data) return self.httpResponse.end(); self.httpResponse.write(data); }); } else { node.get(function(err, body) { if (!Util.empty(err)) return self.handleError(err); // New read/write stream var newStream = new Buffer(offlen); body.copy(newStream, 0, start, offlen); self.httpResponse.writeHead(206, httpHeaders); self.httpResponse.end(newStream); }); } } else { var since = self.httpRequest.headers["if-modified-since"]; var oldEtag = self.httpRequest.headers["if-none-match"]; var lastModified = httpHeaders["last-modified"]; var etag = httpHeaders["etag"]; since = since && Date.parse(since).valueOf(); lastModified = lastModified && Date.parse(lastModified).valueOf(); // If there is no match, then move on. if (!((since && lastModified === since) || (etag && oldEtag === etag))) { if (nodeSize) httpHeaders["content-length"] = nodeSize; if (hasStream) { var writeStreamingHeader = function () { self.httpResponse.writeHead(200, httpHeaders); }; // no start or end means: get all file contents. node.getStream(null, null, function(err, data) { if (err) { if (!writeStreamingHeader) { self.httpResponse.end(); console.error("jsDAV GET error", err); } else { self.handleError(err); } return; } // write header on first incoming buffer if (writeStreamingHeader) { writeStreamingHeader(); writeStreamingHeader = null; } if (!data) return self.httpResponse.end(); self.httpResponse.write(data); }); } else { node.get(function(err, body) { if (!Util.empty(err)) return self.handleError(err); self.httpResponse.writeHead(200, httpHeaders); self.httpResponse.end(body); }); } } else { // Filter out any Content based headers since there // is no content. var newHeaders = {}; Object.keys(httpHeaders).forEach(function(key) { if (key.indexOf("content") < 0) newHeaders[key] = httpHeaders[key]; }); self.httpResponse.writeHead(304, newHeaders); self.httpResponse.end(); } } }); } }; /** * HTTP HEAD * * This method is normally used to take a peak at a url, and only get the * HTTP response headers, without the body. * This is used by clients to determine if a remote file was changed, so * they can use a local cached version, instead of downloading it again * * @return {void} * @throws {Error} */ this.httpHead = function() { var uri = this.getRequestUri(), self = this; this.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) return self.handleError(err); /* This information is only collection for File objects. * Ideally we want to throw 405 Method Not Allowed for every * non-file, but MS Office does not like this */ var headers = {}; if (node.hasFeature(jsDAV_iFile)) { self.getHTTPHeaders(uri, function(err, headersFetched) { if (!Util.empty(err)) return self.handleError(err); headers = headersFetched; if (!headers["content-type"]) headers["content-type"] = "application/octet-stream"; afterHeaders(); }); } else { afterHeaders(); } function afterHeaders() { self.httpResponse.writeHead(200, headers); self.httpResponse.end(); } }); }; /** * HTTP Delete * * The HTTP delete method, deletes a given uri * * @return {void} * @throws {Error} */ this.httpDelete = function() { var uri = this.getRequestUri(); var self = this; this.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) return self.handleError(err); self.dispatchEvent("beforeUnbind", uri, function(stop) { if (stop) return; node["delete"](function(err) { if (!Util.empty(err)) return self.handleError(err); self.markDirty(uri); self.httpResponse.writeHead(204, {"content-length": "0"}); self.httpResponse.end(); self.dispatchEvent("afterDelete", uri); }); }); }); }; /** * WebDAV PROPFIND * * This WebDAV method requests information about an uri resource, or a list * of resources * If a client wants to receive the properties for a single resource it will * add an HTTP Depth: header with a 0 value. * If the value is 1, it means that it also expects a list of sub-resources * (e.g.: files in a directory) * * The request body contains an XML data structure that has a list of * properties the client understands. * The response body is also an xml document, containing information about * every uri resource and the requested properties * * It has to return a HTTP 207 Multi-status status code * * @throws {Error} */ this.httpPropfind = function() { var self = this; this.getRequestBody("utf8", null, false, function(err, data) { if (!Util.empty(err)) return self.handleError(err); //if (jsDAV.debugMode) // Util.log("{" + self.id + "}", "data received " + data); self.parsePropfindRequest(data, function(err, requestedProperties) { if (!Util.empty(err)) return self.handleError(err); var depth = self.getHTTPDepth(1); // The only two options for the depth of a propfind is 0 or 1 if (depth !== 0) depth = 1; // The requested path var path; try { path = self.getRequestUri(); } catch (ex) { return self.handleError(ex); } //if (jsDAV.debugMode) // Util.log("httpPropfind BEFORE getPropertiesForPath '" + path + "';", requestedProperties); self.getPropertiesForPath(path, requestedProperties, depth, function(err, newProperties) { if (!Util.empty(err)) return self.handleError(err); // Normally this header is only needed for OPTIONS responses, however.. // iCal seems to also depend on these being set for PROPFIND. Since // this is not harmful, we'll add it. var features = ["1", "3", "extended-mkcol"]; for (var plugin in self.plugins) { if (!self.plugins[plugin].getFeatures) Util.log("method getFeatures() NOT implemented for plugin " + plugin, "error"); else features = features.concat(self.plugins[plugin].getFeatures()); } // This is a multi-status response. self.httpResponse.writeHead(207, { "content-type": "application/xml; charset=utf-8", "vary": "Brief,Prefer", "DAV": features.join(",") }); var prefer = self.getHTTPPrefer(); self.httpResponse.end(self.generateMultiStatus(newProperties, prefer["return-minimal"])); }); }); }); }; /** * WebDAV PROPPATCH * * This method is called to update properties on a Node. The request is an * XML body with all the mutations. * In this XML body it is specified which properties should be set/updated * and/or deleted * * @return {void} */ this.httpProppatch = function() { var self = this; this.getRequestBody("utf8", null, false, function(err, data) { if (!Util.empty(err)) return self.handleError(err); //if (jsDAV.debugMode) // Util.log("{" + self.id + "}", "data received " + data); self.parseProppatchRequest(data, function(err, newProperties) { if (!Util.empty(err)) return self.handleError(err); self.updateProperties(self.getRequestUri(), newProperties, function(err, result) { if (!Util.empty(err)) return self.handleError(err); var prefer = self.getHTTPPrefer(); if (prefer["return-minimal"]) { // If return-minimal is specified, we only have to check if the // request was succesful, and don't need to return the // multi-status. var prop; var ok = true; for (var code in result) { prop = result[code]; if (parseInt(code, 10) > 299) { ok = false; break; } } if (ok) { self.httpResponse.writeHead(204, { "vary": "Brief, Prefer" }); self.httpResponse.end(); return; } } self.httpResponse.writeHead(207, { "content-type": "application/xml; charset=utf-8", "vary": "Brief, Prefer" }); self.httpResponse.end(self.generateMultiStatus(result)); }); }); }); }; /** * HTTP PUT method * * This HTTP method updates a file, or creates a new one. * If a new resource was created, a 201 Created status code should be returned. * If an existing resource is updated, it's a 200 Ok * * @return {void} */ this.httpPut = function() { var self = this; var uri = this.getRequestUri(); // Intercepting Content-Range if (this.httpRequest.headers["content-range"]) { /* Content-Range is dangerous for PUT requests: PUT per definition stores a full resource. draft-ietf-httpbis-p2-semantics-15 says in section 7.6: An origin server SHOULD reject any PUT request that contains a Content-Range header field, since it might be misinterpreted as partial content (or might be partial content that is being mistakenly PUT as a full representation). Partial content updates are possible by targeting a separately identified resource with state that overlaps a portion of the larger resource, or by using a different method that has been specifically defined for partial updates (for example, the PATCH method defined in [RFC5789]). This clarifies RFC2616 section 9.6: The recipient of the entity MUST NOT ignore any Content-* (e.g. Content-Range) headers that it does not understand or implement and MUST return a 501 (Not Implemented) response in such cases. OTOH is a PUT request with a Content-Range currently the only way to continue an aborted upload request and is supported by curl, mod_dav, Tomcat and others. Since some clients do use this feature which results in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject all PUT requests with a Content-Range for now. */ return this.handleError(new Exc.NotImplemented("PUT with Content-Range is not allowed.")); } // First we'll do a check to see if the resource already exists this.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) { if (err instanceof Exc.FileNotFound) { // If we got here, the resource didn't exist yet. // `data` is set to `null` to use streamed write. self.createFile(uri, null, "binary", function(err, etag) { if (!Util.empty(err)) return self.handleError(err); var headers = {"content-length": "0"}; if (etag) headers.etag = etag; self.httpResponse.writeHead(201, headers); self.httpResponse.end(); self.dispatchEvent("afterWriteContent", uri); }); } else { return self.handleError(err); } } else { var hasStream = !!node.putStream; // Checking If-None-Match and related headers. self.checkPreconditions(false, function(err, redirected) { if (!Util.empty(err)) return self.handleError(err); if (redirected) return false; // If the node is a collection, we'll deny it if (!node.hasFeature(jsDAV_iFile)) return self.handleError(new Exc.Conflict("PUT is not allowed on non-files.")); self.dispatchEvent("beforeWriteContent", uri, node, function(stop) { if (stop) return; if (hasStream) { node.putStream(self, "binary", afterPut); } else { self.getRequestBody("binary", null, false, function(err, body) { if (!Util.empty(err)) return self.handleError(err); node.put(body, "binary", afterPut); }); } function afterPut(err, etag) { if (!Util.empty(err)) return self.handleError(err); var headers = {"content-length": "0"}; if (etag) headers.etag = etag; self.httpResponse.writeHead(200, headers); self.httpResponse.end(); self.dispatchEvent("afterWriteContent", uri); } }); }); } }); }; /** * WebDAV MKCOL * * The MKCOL method is used to create a new collection (directory) on the server * * @return {void} */ this.httpMkcol = function() { var resourceType; var properties = {}; var self = this; var req = this.httpRequest; this.getRequestBody("utf8", null, false, function(err, requestBody) { if (!Util.empty(err)) return self.handleError(err); if (requestBody) { var contentType = req.headers["content-type"]; if (contentType.indexOf("application/xml") !== 0 && contentType.indexOf("text/xml") !== 0) { // We must throw 415 for unsupported mkcol bodies return self.handleError(new Exc.UnsupportedMediaType( "The request body for the MKCOL request must have an xml Content-Type")); } Xml.loadDOMDocument(requestBody, self.server.options.parser, function(err, dom) { var firstChild = dom.firstChild; if (Xml.toClarkNotation(firstChild) !== "{DAV:}mkcol") { // We must throw 415 for unsupport mkcol bodies return self.handleError(new Exc.UnsupportedMediaType( "The request body for the MKCOL request must be a {DAV:}mkcol request construct.")); } var childNode; var i = 0; var c = firstChild.childNodes; var l = c.length; for (; i < l; ++i) { childNode = c[i]; if (Xml.toClarkNotation(childNode) !== "{DAV:}set") continue; properties = Util.extend(properties, Xml.parseProperties(childNode, self.propertyMap)); } if (!properties["{DAV:}resourcetype"]) { return self.handleError(new Exc.BadRequest( "The mkcol request must include a {DAV:}resourcetype property") ); } delete properties["{DAV:}resourcetype"]; resourceType = []; // Need to parse out all the resourcetypes var rtNode = firstChild.getElementsByTagNameNS("urn:DAV", "resourcetype")[0]; for (i = 0, c = rtNode.childNodes, l = c.length; i < l; ++i) resourceType.push(Xml.toClarkNotation(c[i])); afterParse(); }); } else { resourceType = ["{DAV:}collection"]; afterParse(); } function afterParse() { try { var uri = self.getRequestUri() } catch (ex) { return self.handleError(ex); } self.createCollection(uri, resourceType, properties, function(err, result) { if (!Util.empty(err)) return self.handleError(err); if (result && result.length) { self.httpResponse.writeHead(207, {"content-type": "application/xml; charset=utf-8"}); self.httpResponse.end(self.generateMultiStatus(result)); } else { self.httpResponse.writeHead(201, {"content-length": "0"}); self.httpResponse.end(); } }); } }); }; /** * WebDAV HTTP MOVE method * * This method moves one uri to a different uri. A lot of the actual request * processing is done in getCopyMoveInfo * * @return {void} */ this.httpMove = function() { var self = this; this.getCopyAndMoveInfo(function(err, moveInfo) { if (!Util.empty(err)) return self.handleError(err); if (moveInfo.destinationExists) { self.dispatchEvent("beforeUnbind", moveInfo.destination, function(stop) { if (stop) return false; moveInfo.destinationNode["delete"](function(err) { if (!Util.empty(err)) return self.handleError(err); afterDelete(); }); }); } else { afterDelete(); } function afterDelete() { self.dispatchEvent("beforeUnbind", moveInfo.source, function(stop) { if (stop) return false; self.dispatchEvent("beforeBind", moveInfo.destination, function(stop) { if (stop) return false; self.server.tree.move(moveInfo.source, moveInfo.destination, function(err, sourceDir, destinationDir) { if (!Util.empty(err)) return self.handleError(err); self.markDirty(moveInfo.source); self.markDirty(moveInfo.destination); self.dispatchEvent("afterBind", moveInfo.destination, Path.join(self.server.tree.basePath, moveInfo.destination)); // If a resource was overwritten we should send a 204, otherwise a 201 self.httpResponse.writeHead(moveInfo.destinationExists ? 204 : 201, {"content-length": "0"}); self.httpResponse.end(); self.dispatchEvent("afterMove", moveInfo.destination); }); }); }); } }); }; /** * WebDAV HTTP COPY method * * This method copies one uri to a different uri, and works much like the MOVE request * A lot of the actual request processing is done in getCopyMoveInfo * * @return {void} */ this.httpCopy = function() { var self = this; this.getCopyAndMoveInfo(function(err, copyInfo) { if (!Util.empty(err)) return self.handleError(err); if (copyInfo.destinationExists) { self.dispatchEvent("beforeUnbind", copyInfo.destination, function(stop) { if (stop) return false; copyInfo.destinationNode["delete"](function(err) { if (!Util.empty(err)) return self.handleError(err); afterDelete(); }); }); } else { afterDelete(); } function afterDelete() { self.dispatchEvent("beforeBind", copyInfo.destination, function(stop) { if (stop) return false; self.server.tree.copy(copyInfo.source, copyInfo.destination, function(err) { if (!Util.empty(err)) return self.handleError(err); self.markDirty(copyInfo.destination); self.dispatchEvent("afterBind", copyInfo.destination, Path.join(self.server.tree.basePath, copyInfo.destination)); // If a resource was overwritten we should send a 204, otherwise a 201 self.httpResponse.writeHead(copyInfo.destinationExists ? 204 : 201, {"Content-Length": "0"}); self.httpResponse.end(); self.dispatchEvent("afterCopy", copyInfo.destination); }); }); } }); }; /** * HTTP REPORT method implementation * * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) * It's used in a lot of extensions, so it made sense to implement it into the core. * * @return {void} */ this.httpReport = function() { var self = this; this.getRequestBody("utf8", null, false, function(err, data) { Xml.loadDOMDocument(data, self.server.options.parser, function(err, dom) { var reportName = Xml.toClarkNotation(dom); self.dispatchEvent("report", reportName, dom, function(stop) { if (stop !== true) { // if dispatchEvent didn't return true, it means the report was not supported return self.handleError(new Exc.ReportNotImplemented()); } }); }); }); }; /** * Returns an array with all the supported HTTP methods for a specific uri. * * @param {String} uri * @param {Function} cbmethods Callback that is the return body of this function * @return {Array} */ this.getAllowedMethods = function(uri, cbmethods) { var self = this; var methods = [ "OPTIONS", "GET", "HEAD", "DELETE", "PROPFIND", "PUT", "PROPPATCH", "COPY", "MOVE", "REPORT" ]; // The MKCOL is only allowed on an unmapped uri this.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) methods.push("MKCOL"); // We're also checking if any of the plugins register any new methods for (var plugin in self.plugins) { if (!self.plugins[plugin].getHTTPMethods) Util.log("method getHTTPMethods() NOT implemented for plugin " + plugin, "error"); else methods = methods.concat(self.plugins[plugin].getHTTPMethods(uri, node)); } cbmethods(null, Util.makeUnique(methods)); }); }; /** * Gets the uri for the request, keeping the base uri into consideration * * @return {String} * @throws {Error} */ this.getRequestUri = function() { return this.calculateUri(this.httpRequest.url); }; /** * Fetch the binary data for the HTTP request and return it to a callback OR * write it to a WritableStream instance. * * @param {String} enc * @param {WritableStream} [stream] * @param {Boolean} [forceStream] * @param {Function} callback * @return {void} */ this.getRequestBody = function(enc, stream, forceStream, cbreqbody) { if (!cbreqbody) { cbreqbody = stream; stream = null; forceStream = false; } var ctype; var self = this; var req = this.httpRequest; var isStream = forceStream === true ? true : (!(ctype = req.headers["content-type"]) || !ctype.match(/(urlencoded|multipart)/i)); // HACK: MacOSX Finder and NodeJS don't play nice together with files // that start with '._' //if (/\/\.[D_]{1}[^\/]+$/.test(req.url)) // return cbreqbody(null, "", cleanup); enc = (enc || "utf8").replace("-", ""); if (enc == "raw") enc = "binary"; if (req.$data) { return cbreqbody(req.$data.err || null, enc == "binary" ? req.$data : req.$data.toString(enc)); } if (isStream) { var buff = []; var contentLength = req.headers["content-length"]; var lengthCount = 0; var err; if (stream) stream.on("error", function(ex) { err = ex; }); req.streambuffer.ondata(function(data) { lengthCount += data.length; if (stream && stream.writable) stream.write(data); else buff.push(data); }); req.streambuffer.onend(function() { // TODO: content-length check and rollback... if (stream) { if (err) return cbreqbody(err); stream.on("close", function() { cbreqbody(err); });