// ----------------------------------------------------------------------
// File: PropFindResponse.cc
// Author: Justin Lewis Salmon - CERN
// ----------------------------------------------------------------------
/************************************************************************
* EOS - the CERN Disk Storage System *
* Copyright (C) 2013 CERN/Switzerland *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see .*
************************************************************************/
#include "mgm/http/webdav/PropFindResponse.hh"
#include "mgm/XrdMgmOfs.hh"
#include "mgm/XrdMgmOfsDirectory.hh"
#include "mgm/Quota.hh"
#include "mgm/Access.hh"
#include "mgm/XrdMgmOfs.hh"
#include "mgm/Macros.hh"
#include "common/Logging.hh"
#include "common/Timing.hh"
#include "common/Path.hh"
#include "common/http/OwnCloud.hh"
#include "XrdOuc/XrdOucErrInfo.hh"
EOSMGMNAMESPACE_BEGIN
/*----------------------------------------------------------------------------*/
char dav_rfc3986[256] = {0};
char dav_html5[256] = {0};
/*----------------------------------------------------------------------------*/
void
dav_uri_encode(unsigned char* s, char* enc, char* tb)
{
for (; *s; s++) {
if (tb[*s]) {
sprintf(enc, "%c", tb[*s]);
} else {
sprintf(enc, "%%%02X", *s);
}
while (*++enc);
}
}
int
dav_uri_decode(char* source, char* dest)
{
int nLength;
for (nLength = 0; *source; nLength++) {
dest[nLength + 1] = 0;
if (*source == '%' && source[1] && source[2] && isxdigit(source[1]) &&
isxdigit(source[2])) {
source[1] -= source[1] <= '9' ? '0' : (source[1] <= 'F' ? 'A' : 'a') - 10;
source[2] -= source[2] <= '9' ? '0' : (source[2] <= 'F' ? 'A' : 'a') - 10;
dest[nLength] = 16 * source[1] + source[2];
source += 3;
continue;
}
dest[nLength] = *source++;
}
dest[nLength] = '\0';
return nLength;
}
/*----------------------------------------------------------------------------*/
std::string
PropFindResponse::EncodeURI(const char* uri)
{
XrdOucString nUri;
char enc[(strlen(uri) + 1) * 3];
dav_uri_encode((unsigned char*) uri, enc, dav_rfc3986);
return std::string(enc);
}
/*----------------------------------------------------------------------------*/
eos::common::HttpResponse*
PropFindResponse::BuildResponse(eos::common::HttpRequest* request)
{
using namespace rapidxml;
// Get the namespaces (if any)
ParseNamespaces();
eos_static_debug("\n%s", request->GetBody().c_str());
// Root node
xml_node<>* rootNode = mXMLRequestDocument.first_node();
if (!rootNode) {
SetResponseCode(ResponseCodes::BAD_REQUEST);
return this;
}
// Get the requested property types
ParseRequestPropertyTypes(rootNode);
if (mRequestPropertyTypes & PropertyTypes::GET_OCID) {
XrdOucErrInfo error;
XrdOucString val;
eos::common::VirtualIdentity rootvid = eos::common::VirtualIdentity::Root();
if (gOFS->_attr_get(request->GetUrl().c_str(), error, rootvid, "",
eos::common::OwnCloud::GetAllowSyncName(), val)) {
// Sync not allowed in this tree.
SetResponseCode(ResponseCodes::FORBIDDEN);
return this;
}
}
// Build the response
// xml declaration
xml_node<>* decl = mXMLResponseDocument.allocate_node(node_declaration);
decl->append_attribute(AllocateAttribute("version", "1.0"));
decl->append_attribute(AllocateAttribute("encoding", "utf-8"));
mXMLResponseDocument.append_node(decl);
// node
xml_node<>* multistatusNode = AllocateNode("d:multistatus");
multistatusNode->append_attribute(AllocateAttribute("xmlns:d", "DAV:"));
multistatusNode->append_attribute(
AllocateAttribute(eos::common::OwnCloud::OwnCloudNs(),
eos::common::OwnCloud::OwnCloudNsUrl()));
mXMLResponseDocument.append_node(multistatusNode);
// Is the requested resource a file or directory?
XrdOucErrInfo error;
struct stat statInfo;
std::string etag;
memset(&statInfo, 0, sizeof(struct stat));
// TODO: the status should be chcked ?!
std::string raw_path = request->GetUrl().c_str();
eos::mgm::NamespaceMap(raw_path, nullptr, *mVirtualIdentity);
(void) gOFS->_stat(raw_path.c_str(), &statInfo, error,
*mVirtualIdentity, (const char*) 0, &etag);
// Figure out what we actually need to do
std::string depth = request->GetHeaders()["depth"];
eos_static_debug("depth=%s, isdir=%d", depth.c_str(),
S_ISDIR(statInfo.st_mode));
xml_node<>* responseNode = 0;
if (depth == "0" || !S_ISDIR(statInfo.st_mode)) {
// Simply stat the file or direcAtory
responseNode = BuildResponseNode(request->GetUrl(), request->GetUrl(true));
if (responseNode) {
multistatusNode->append_node(responseNode);
} else {
return this;
}
} else if (depth == "1") {
// Stat the resource and all child resources
XrdMgmOfsDirectory directory;
int listrc = directory.open(request->GetUrl().c_str(), *mVirtualIdentity,
(const char*) 0);
responseNode = BuildResponseNode(request->GetUrl().c_str(),
request->GetUrl(true).c_str());
if (responseNode) {
multistatusNode->append_node(responseNode);
}
if (!listrc) {
const char* val;
while ((val = directory.nextEntry())) {
XrdOucString entryname = val;
// don't display . .., atomic(+version) uploads and version directories
if (entryname.beginswith(EOS_COMMON_PATH_VERSION_FILE_PREFIX) ||
entryname.beginswith(EOS_COMMON_PATH_ATOMIC_FILE_PREFIX) ||
entryname.beginswith(EOS_WEBDAV_HIDE_IN_PROPFIND_PREFIX) ||
entryname.beginswith("...eos.ino...") ||
(entryname == ".") ||
(entryname == "..")) {
// skip over . .., and hidden files
continue;
}
// one response node for each file...
eos::common::Path path((request->GetUrl() + std::string("/") + std::string(
val)).c_str());
eos::common::Path refpath((request->GetUrl(true) + std::string("/") +
std::string(val)).c_str());
responseNode = BuildResponseNode(path.GetPath(), refpath.GetPath());
if (responseNode) {
multistatusNode->append_node(responseNode);
} else {
// We might have a failed stat in the BuildResponseNode if there are
// symlinks present
SetResponseCode(HttpResponse::OK);
}
}
} else {
eos_static_warning("msg=\"error opening directory - might be stalled/banned\"");
SetResponseCode(ResponseCodes::FORBIDDEN);
return this;
}
} else if (depth == "1,noroot") {
// Stat all child resources but not the requested resource
SetResponseCode(HttpResponse::NOT_IMPLEMENTED);
return this;
} else if (depth == "infinity" || depth == "") {
// Recursively stat the resource and all child resources
SetResponseCode(HttpResponse::NOT_IMPLEMENTED);
return this;
}
std::string responseString;
rapidxml::print(std::back_inserter(responseString), mXMLResponseDocument,
rapidxml::print_no_indenting);
mXMLResponseDocument.clear();
SetResponseCode(HttpResponse::MULTI_STATUS);
AddHeader("Content-Length", std::to_string((long long) responseString.size()));
AddHeader("Content-Type", "application/xml; charset=utf-8");
SetBody(responseString);
return this;
}
/*----------------------------------------------------------------------------*/
void
PropFindResponse::ParseRequestPropertyTypes(rapidxml::xml_node<>* node)
{
using namespace rapidxml;
// node (could be multiple, could be )
xml_node<>* allpropNode = GetNode(node, "allprop");
if (allpropNode) {
mRequestPropertyTypes |= PropertyTypes::GET_CONTENT_LENGTH;
mRequestPropertyTypes |= PropertyTypes::GET_CONTENT_TYPE;
mRequestPropertyTypes |= PropertyTypes::GET_LAST_MODIFIED;
mRequestPropertyTypes |= PropertyTypes::GET_ETAG;
mRequestPropertyTypes |= PropertyTypes::CREATION_DATE;
mRequestPropertyTypes |= PropertyTypes::DISPLAY_NAME;
mRequestPropertyTypes |= PropertyTypes::RESOURCE_TYPE;
mRequestPropertyTypes |= PropertyTypes::CHECKED_IN;
mRequestPropertyTypes |= PropertyTypes::CHECKED_OUT;
mRequestPropertyTypes |= PropertyTypes::ALLPROP_MARKER;
return;
}
// It wasn't
xml_node<>* propNode = GetNode(node, "prop");
if (!propNode) {
eos_static_err("msg=\"no node found in tree\"");
return;
}
xml_node<>* property = propNode->first_node();
// Find all the request properties
while (property) {
XrdOucString propertyName = property->name();
eos_static_debug("msg=\"found xml property: %s\"", propertyName.c_str());
int colon = 0;
if ((colon = propertyName.find(':')) != STR_NPOS) {
// Split node name into :
// Ignore non DAV: namespaces for now
for (auto it = mDAVNamespaces.begin(); it != mDAVNamespaces.end(); ++it) {
std::string ns = it->first;
if (propertyName.beginswith(ns.c_str())) {
std::string prop(std::string(propertyName.c_str()), colon + 1);
mRequestPropertyTypes |= MapRequestPropertyType(prop);
}
}
for (auto it = mCustomNamespaces.begin(); it != mCustomNamespaces.end(); ++it) {
std::string ns = it->first;
if (propertyName.beginswith(ns.c_str())) {
std::string prop(std::string(propertyName.c_str()), colon + 1);
mRequestPropertyTypes |= MapRequestPropertyType(prop);
}
}
} else {
std::string prop(propertyName.c_str());
mRequestPropertyTypes |= MapRequestPropertyType(prop);
}
property = property->next_sibling();
}
}
/*----------------------------------------------------------------------------*/
rapidxml::xml_node<>*
PropFindResponse::BuildResponseNode(const std::string& url,
const std::string& hrefurl)
{
using namespace rapidxml;
XrdOucErrInfo error;
struct stat statInfo;
std::string etag;
std::string id;
bool allpropresponse = false;
XrdOucString urlp = url.c_str();
XrdOucString hrefp = hrefurl.c_str();
while (urlp.replace("//", "/")) {
}
while (hrefp.replace("//", "/")) {
}
// Is the requested resource a file or directory?
std::string raw_path = urlp.c_str();
eos::mgm::NamespaceMap(raw_path, nullptr, *mVirtualIdentity);
eos_static_debug("url_path=%s raw_path=%s", urlp.c_str(), raw_path.c_str());
if (gOFS->_stat(raw_path.c_str(), &statInfo, error, *mVirtualIdentity,
(const char*) 0, &etag)) {
eos_static_err("msg=\"error stating %s: %s\"", urlp.c_str(),
error.getErrText());
if (error.getErrInfo() == EACCES) {
SetResponseCode(ResponseCodes::FORBIDDEN);
} else {
SetResponseCode(ResponseCodes::NOT_FOUND);
}
return NULL;
}
// hide hardlinks
if (etag == "hardlink") {
// this is the 'best' guess to identify a hardlink entry (for now)
eos_static_err("msg=\"hiding hardlinkg %s: %s\"", urlp.c_str(),
error.getErrText());
SetResponseCode(ResponseCodes::NOT_FOUND);
return NULL;
}
eos_static_debug("url=%s etag=%s", urlp.c_str(), etag.c_str());
// encode the url's
urlp = EncodeURI(urlp.c_str()).c_str();
hrefp = EncodeURI(hrefp.c_str()).c_str();
// node
xml_node<>* responseNode = AllocateNode("d:response");
// node
xml_node<>* href = AllocateNode("d:href");
if (S_ISDIR(statInfo.st_mode)) {
if (hrefp[hrefp.length() - 1] != '/') {
hrefp += "/";
}
}
SetValue(href, hrefp.c_str());
responseNode->append_node(href);
// node for "found" properties
xml_node<>* propstatFound = AllocateNode("d:propstat");
responseNode->append_node(propstatFound);
// "found" node
xml_node<>* statusFound = AllocateNode("d:status");
SetValue(statusFound, "HTTP/1.1 200 OK");
propstatFound->append_node(statusFound);
// "found" node
xml_node<>* propFound = AllocateNode("d:prop");
propstatFound->append_node(propFound);
// node for "not found" properties
xml_node<>* propstatNotFound = AllocateNode("d:propstat");
responseNode->append_node(propstatNotFound);
// "not found" node
xml_node<>* statusNotFound = AllocateNode("d:status");
SetValue(statusNotFound, "HTTP/1.1 404 Not Found");
propstatNotFound->append_node(statusNotFound);
// "not found" node
xml_node<>* propNotFound = AllocateNode("d:prop");
propstatNotFound->append_node(propNotFound);
xml_node<>* contentLength = 0;
xml_node<>* lastModified = 0;
xml_node<>* resourceType = 0;
xml_node<>* checkedIn = 0;
xml_node<>* checkedOut = 0;
xml_node<>* creationDate = 0;
xml_node<>* eTag = 0;
xml_node<>* displayName = 0;
xml_node<>* contentType = 0;
xml_node<>* quotaAvail = 0;
xml_node<>* quotaUsed = 0;
xml_node<>* ocid = 0;
xml_node<>* ocsize = 0;
xml_node<>* ocperm = 0;
if (mRequestPropertyTypes & PropertyTypes::GET_CONTENT_LENGTH) {
contentLength = AllocateNode("d:getcontentlength");
}
if (mRequestPropertyTypes & PropertyTypes::GET_CONTENT_TYPE) {
contentType = AllocateNode("d:getcontenttype");
}
if (mRequestPropertyTypes & PropertyTypes::GET_LAST_MODIFIED) {
lastModified = AllocateNode("d:getlastmodified");
}
if (mRequestPropertyTypes & PropertyTypes::CREATION_DATE) {
creationDate = AllocateNode("d:creationdate");
}
if (mRequestPropertyTypes & PropertyTypes::RESOURCE_TYPE) {
resourceType = AllocateNode("d:resourcetype");
}
if (mRequestPropertyTypes & PropertyTypes::DISPLAY_NAME) {
displayName = AllocateNode("d:displayname");
}
if (mRequestPropertyTypes & PropertyTypes::GET_ETAG) {
eTag = AllocateNode("d:getetag");
}
if (mRequestPropertyTypes & PropertyTypes::CHECKED_IN) {
checkedIn = AllocateNode("d:checked-in");
}
if (mRequestPropertyTypes & PropertyTypes::CHECKED_OUT) {
checkedOut = AllocateNode("d:checked-out");
}
if (mRequestPropertyTypes & PropertyTypes::GET_OCID) {
ocid = AllocateNode("oc:id");
}
if (mRequestPropertyTypes & PropertyTypes::GET_OCSIZE) {
ocsize = AllocateNode("oc:size");
}
if (mRequestPropertyTypes & PropertyTypes::GET_OCPERM) {
ocperm = AllocateNode("oc:permissions");
}
if (mRequestPropertyTypes & PropertyTypes::ALLPROP_MARKER) {
allpropresponse = true;
}
if ((S_ISDIR(statInfo.st_mode)) &&
((mRequestPropertyTypes & PropertyTypes::QUOTA_AVAIL) ||
(mRequestPropertyTypes & PropertyTypes::QUOTA_USED))) {
// -----------------------------------------------------------
// retrieve the current quota
// -----------------------------------------------------------
XrdOucString path = url.c_str();
if (!path.endswith("/")) {
path += "/";
}
while (path.replace("//", "/")) {}
long long maxbytes = 0;
long long freebytes = 0;
long long maxfiles = 0;
long long freefiles = 0;
Quota::GetIndividualQuota(*mVirtualIdentity, path.c_str(), maxbytes, freebytes,
maxfiles, freefiles, true);
if (mRequestPropertyTypes & PropertyTypes::QUOTA_AVAIL) {
std::string sQuotaAvail;
quotaAvail = AllocateNode("d:quota-available-bytes");
if (quotaAvail) {
SetValue(quotaAvail, eos::common::StringConversion::GetSizeString(sQuotaAvail,
(unsigned long long) freebytes));
}
}
if (mRequestPropertyTypes & PropertyTypes::QUOTA_USED) {
std::string sQuotaUsed;
quotaUsed = AllocateNode("d:quota-used-bytes");
SetValue(quotaUsed, eos::common::StringConversion::GetSizeString(sQuotaUsed,
(unsigned long long) statInfo.st_size));
}
}
// getlastmodified, creationdate, displayname and getetag properties are
// common to all resources
if (lastModified) {
std::string lm = eos::common::Timing::utctime(
statInfo.st_mtim.tv_sec);
SetValue(lastModified, lm.c_str());
propFound->append_node(lastModified);
}
if (creationDate) {
std::string cd = eos::common::Timing::UnixTimestamp_to_ISO8601(
statInfo.st_ctim.tv_sec);
SetValue(creationDate, cd.c_str());
propFound->append_node(creationDate);
}
if (eTag) {
SetValue(eTag, etag.c_str());
propFound->append_node(eTag);
}
if (ocid) {
SetValue(ocid, eos::common::StringConversion::GetSizeString(id,
(unsigned long long) statInfo.st_ino));
propFound->append_node(ocid);
}
if (ocsize) {
SetValue(ocsize, eos::common::StringConversion::GetSizeString(id,
(unsigned long long) statInfo.st_size));
propFound->append_node(ocsize);
}
if (ocperm) {
// test access permissions
std::string oc_perm = "";
gOFS->acc_access(url.c_str(), error, *mVirtualIdentity, oc_perm);
SetValue(ocperm, oc_perm.c_str());
propFound->append_node(ocperm);
}
if (displayName) {
eos::common::Path path(urlp.c_str());
eos_static_debug("msg=\"display name: %s\"", path.GetName());
SetValue(displayName, path.GetName());
propFound->append_node(displayName);
}
// Directory
if (S_ISDIR(statInfo.st_mode)) {
if (resourceType) {
xml_node<>* container = AllocateNode("d:collection");
resourceType->append_node(container);
propFound->append_node(resourceType);
}
if (!allpropresponse) {
// ANDROID does not digest this response properly in allprop requests
if (contentLength) {
propNotFound->append_node(contentLength);
}
}
if (contentType) {
SetValue(contentType, "httpd/unix-directory");
propFound->append_node(contentType);
}
if (quotaAvail) {
propFound->append_node(quotaAvail);
}
if (quotaUsed) {
propFound->append_node(quotaUsed);
}
}
// File
else {
if (resourceType) {
propFound->append_node(resourceType);
}
if (contentLength) {
SetValue(contentLength, std::to_string((long long) statInfo.st_size).c_str());
propFound->append_node(contentLength);
}
if (contentType) {
SetValue(contentType, HttpResponse::ContentType(url.c_str()).c_str());
propFound->append_node(contentType);
}
}
// We don't use these (yet)
if (!allpropresponse) {
// ANDROID does not digest this response properly in allprop requests
if (checkedIn) {
propNotFound->append_node(checkedIn);
}
if (checkedOut) {
propNotFound->append_node(checkedOut);
}
}
return responseNode;
}
/*----------------------------------------------------------------------------*/
EOSMGMNAMESPACE_END