// ---------------------------------------------------------------------- // File: HttpHandler.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/HttpServer.hh" #include "mgm/http/HttpHandler.hh" #include "mgm/XrdMgmOfsDirectory.hh" #include "mgm/XrdMgmOfs.hh" #include "mgm/Stat.hh" #include "common/Timing.hh" #include "common/ErrnoToString.hh" #include "common/http/PlainHttpResponse.hh" #include "common/http/OwnCloud.hh" #include "namespace/utils/Mode.hh" #include "mgm/http/rest-api/handler/tape/TapeRestHandler.hh" #include "mgm/http/rest-api/manager/RestApiManager.hh" EOSMGMNAMESPACE_BEGIN /*----------------------------------------------------------------------------*/ bool HttpHandler::Matches(const std::string& meth, HeaderMap& headers) { int method = ParseMethodString(meth); if (method == GET || method == HEAD || method == POST || method == PUT || method == DELETE || method == TRACE || method == OPTIONS || method == CONNECT || method == PATCH) { eos_static_debug("Matched HTTP protocol for request"); return true; } else { return false; } } /*----------------------------------------------------------------------------*/ void HttpHandler::HandleRequest(eos::common::HttpRequest* request) { eos_static_debug("handling http request"); eos::common::HttpResponse* response = 0; bool isRestRequest = gOFS->mRestApiManager->isRestRequest(request->GetUrl()); if (isRestRequest) { response = gOFS->mRestApiManager->getRestHandler( request->GetUrl())->handleRequest(request, mVirtualIdentity); } else { request->AddEosApp(); for (auto it = request->GetHeaders().begin(); it != request->GetHeaders().end(); ++it) { eos_static_info("header:%s => %s", it->first.c_str(), it->second.c_str()); } int meth = ParseMethodString(request->GetMethod()); { // call the routing module before doing anything with http int port; std::string host; int stall_timeout = 0; if (gOFS->ShouldRoute( __FUNCTION__, 0, *mVirtualIdentity, request->GetUrl().c_str(), request->GetQuery().c_str(), host, port, stall_timeout)) { response = HttpServer::HttpRedirect(request->GetUrl().c_str(), host.c_str(), port, false); mHttpResponse = response; return; } } switch (meth) { case GET: gOFS->MgmStats.Add("Http-GET", mVirtualIdentity->uid, mVirtualIdentity->gid, 1); response = Get(request); break; case HEAD: gOFS->MgmStats.Add("Http-HEAD", mVirtualIdentity->uid, mVirtualIdentity->gid, 1); response = Head(request); response->SetBody(""); break; case POST: gOFS->MgmStats.Add("Http-POST", mVirtualIdentity->uid, mVirtualIdentity->gid, 1); response = Post(request); break; case PUT: gOFS->MgmStats.Add("Http-PUT", mVirtualIdentity->uid, mVirtualIdentity->gid, 1); response = Put(request); break; case DELETE: gOFS->MgmStats.Add("Http-DELETE", mVirtualIdentity->uid, mVirtualIdentity->gid, 1); response = Delete(request); break; case TRACE: gOFS->MgmStats.Add("Http-TRACE", mVirtualIdentity->uid, mVirtualIdentity->gid, 1); response = Trace(request); break; case OPTIONS: gOFS->MgmStats.Add("Http-OPTIONS", mVirtualIdentity->uid, mVirtualIdentity->gid, 1); response = Options(request); break; case CONNECT: gOFS->MgmStats.Add("Http-CONNECT", mVirtualIdentity->uid, mVirtualIdentity->gid, 1); response = Connect(request); break; case PATCH: gOFS->MgmStats.Add("Http-PATCH", mVirtualIdentity->uid, mVirtualIdentity->gid, 1); response = Patch(request); break; default: response = new eos::common::PlainHttpResponse(); response->SetResponseCode(eos::common::HttpResponse::BAD_REQUEST); response->SetBody("No such method"); } } mHttpResponse = response; } /*----------------------------------------------------------------------------*/ eos::common::HttpResponse* HttpHandler::Get(eos::common::HttpRequest* request, bool isHEAD) { using eos::common::ErrnoToString; XrdSecEntity client(mVirtualIdentity->prot.c_str()); client.name = const_cast(mVirtualIdentity->uid_string.c_str()); client.host = const_cast(mVirtualIdentity->host.c_str()); client.tident = const_cast(mVirtualIdentity->tident.c_str()); // Classify path to split between directory or file objects bool isfile = true; std::string url = request->GetUrl(); std::string query = request->GetQuery(); eos::common::HttpResponse* response = 0; struct stat buf; XrdOucString spath = request->GetUrl().c_str(); // redirect '/' to '/eos//' if (spath == "/") { XrdOucString instance = gOFS->MgmOfsInstanceName; if (instance.beginswith("eos")) { instance.replace("eos", ""); } response = HttpServer::HttpRedirect(url + "eos/" + instance.c_str(), gOFS->HostName, gOFS->mHttpdPort, false); return response; } std::string etag = "undef"; eos::common::OwnCloud::OwnCloudRemapping(spath, request); eos::common::OwnCloud::ReplaceRemotePhp(spath); if (!spath.beginswith("/proc/")) { XrdOucErrInfo error(mVirtualIdentity->tident.c_str()); { // check if this is a symlink XrdOucString link; if ((!gOFS->_readlink(url.c_str(), error, *mVirtualIdentity, link)) && (link != "") && (link.beginswith("http://") || link.beginswith("https://"))) { if (gOFS->access(url.c_str(), R_OK, error, &client, query.c_str())) { // no permission or entry doesn't exist eos_static_info("method=GET error=%i path=%s", error.getErrInfo(), url.c_str()); response = HttpServer::HttpError(ErrnoToString(error.getErrInfo()).c_str(), (error.getErrInfo() == ENOENT) ? response->NOT_FOUND : response->FORBIDDEN); return response; } // create an external redirect response = new eos::common::PlainHttpResponse(); response->SetResponseCode( eos::common::HttpResponse::ResponseCodes::TEMPORARY_REDIRECT); response->AddHeader("Location", link.c_str()); response->AddHeader("X-Accel-Redirect", link.c_str()); response->AddHeader("X-Sendfile", link.c_str()); return response; } else { if (gOFS->access(url.c_str(), R_OK, error, &client, query.c_str())) { // no permission or entry doesn't exist eos_static_info("method=GET error=%i path=%s", error.getErrInfo(), url.c_str()); response = HttpServer::HttpError(ErrnoToString(error.getErrInfo()).c_str(), (error.getErrInfo() == ENOENT) ? response->NOT_FOUND : response->FORBIDDEN); return response; } } } if (gOFS->stat(url.c_str(), &buf, error, &etag, &client, query.c_str())) { eos_static_info("method=GET error=ENOENT path=%s", url.c_str()); response = HttpServer::HttpError("No such file or directory", response->NOT_FOUND); return response; } if (gOFS->stat(url.c_str(), &buf, error, &etag, &client, query.c_str())) { eos_static_info("method=GET error=ENOENT path=%s", url.c_str()); response = HttpServer::HttpError("No such file or directory", response->NOT_FOUND); return response; } if (request->GetHeaders().count("if-match") && (etag != request->GetHeaders()["if-match"])) { // ETag mismatch eos_static_info("method=GET error=precondition-failed path=%s etag=%s cond=match r-etag=%s", url.c_str(), etag.c_str(), request->GetHeaders()["If-Match"].c_str()); response = HttpServer::HttpError("ETag precondition failed", response->PRECONDITION_FAILED); return response; } if (request->GetHeaders().count("if-non-match") && (etag == request->GetHeaders()["if-non-match"])) { // ETag match eos_static_info("method=GET error=precondition-failed path=%s etag=%s cond=not-match r-etag=%s", url.c_str(), etag.c_str(), request->GetHeaders()["if-not-match"].c_str()); response = HttpServer::HttpError("ETag is not modified", response->NOT_MODIFIED); return response; } // find out if it is a file or directory if (S_ISDIR(buf.st_mode)) { isfile = false; if (isHEAD) { // HEAD requests for dirs just act like 'exists' eos_static_info("cmd=GET(HEAD) size=%llu path=%s type=dir", buf.st_size, url.c_str()); response = new eos::common::PlainHttpResponse(); response->SetBody(""); response->AddHeader("ETag", etag); response->AddHeader("Last-Modified", eos::common::Timing::utctime(buf.st_mtime)); return response; } } else { isfile = true; if (isHEAD) { std::string basename = url.substr(url.rfind('/') + 1); eos_static_info("cmd=GET(HEAD) size=%llu path=%s type=file", buf.st_size, url.c_str()); // HEAD requests on files can return from the MGM without redirection response = HttpServer::HttpHead(buf.st_size, basename); response->AddHeader("ETag", etag); response->AddHeader("Last-Modified", eos::common::Timing::utctime(buf.st_mtime)); if (request->GetHeaders().count("want-digest")) { std::string type = request->GetHeaders()["want-digest"]; type = LC_STRING(type); XrdOucString digest = ""; eos_static_debug("method=HEAD, path=%s, checksum requested=%s", url.c_str(), type.c_str()); //check if there is a checksum type and checksum std::string xstype; std::string xs; if (!gOFS->_getchecksum(url.c_str(), error, &xstype, &xs, &client, query.c_str())) { //check if the type match what requested if (xstype == type) { eos_static_debug("method=HEAD, path=%s, checksum requested=%s, checksum available=%s", url.c_str(), type.c_str(), xstype.c_str()); digest += xstype.c_str(); digest += "="; digest += xs.c_str(); response->AddHeader("Digest", digest.c_str()); } } } return response; } } } if (!isfile) { eos_static_info("method=GET dir=%s", url.c_str()); errno = 0; { // Check if there is an index attribute XrdOucString index; XrdOucErrInfo error(mVirtualIdentity->tident.c_str()); if (!gOFS->_attr_get(url.c_str(), error, *mVirtualIdentity, query.c_str(), "sys.http.index", index)) { if (gOFS->access(url.c_str(), R_OK, error, &client, query.c_str())) { // no permission or entry doesn't exist eos_static_info("method=GET error=%i path=%s", error.getErrInfo(), url.c_str()); response = HttpServer::HttpError(ErrnoToString(error.getErrInfo()).c_str(), (error.getErrInfo() == ENOENT) ? response->NOT_FOUND : response->FORBIDDEN); return response; } // create an external redirect response = new eos::common::PlainHttpResponse(); response->SetResponseCode( eos::common::HttpResponse::ResponseCodes::TEMPORARY_REDIRECT); response->AddHeader("Location", index.c_str()); response->AddHeader("X-Accel-Redirect", index.c_str()); response->AddHeader("X-Sendfile", index.c_str()); return response; } } response = HttpServer::HttpError("Browsing is disabled and no index attribute is defined!", response->FORBIDDEN); return response; } else { eos_static_info("method=GET file=%s tident=%s query=%s", url.c_str(), client.tident, query.c_str()); XrdSfsFile* file = gOFS->newFile((char*) mVirtualIdentity->tident.c_str()); if (file) { XrdSfsFileOpenMode open_mode = 0; mode_t create_mode = 0; int rc = file->open(url.c_str(), open_mode, create_mode, &client, query.c_str()); // TODO (apeters): review this part - dead code open_mode = 0 if ((rc != SFS_REDIRECT) && open_mode) { // retry as a file creation open_mode |= SFS_O_CREAT; rc = file->open(url.c_str(), open_mode, create_mode, &client, query.c_str()); } if (rc != SFS_OK) { if (rc == SFS_REDIRECT) { std::string urlenc = eos::common::StringConversion::curl_path_escaped(request->GetUrl().c_str()); response = HttpServer::HttpRedirect(urlenc, file->error.getErrText(), file->error.getErrInfo(), false); } else if (rc == SFS_ERROR) { response = HttpServer::HttpError(file->error.getErrText(), file->error.getErrInfo()); } else if (rc == SFS_DATA) { response = HttpServer::HttpData(file->error.getErrText(), file->error.getErrInfo()); } else if (rc == SFS_STALL) { response = HttpServer::HttpStall(file->error.getErrText(), file->error.getErrInfo()); } else { response = HttpServer::HttpError("Unexpected result from file open", EOPNOTSUPP); } response->AddHeader("ETag", etag); } else { char buffer[65536]; offset_t offset = 0; std::string result; do { size_t nread = file->read(offset, buffer, sizeof(buffer)); if (nread > 0) { result.append(buffer, nread); } if (nread != sizeof(buffer)) { break; } offset += nread; } while (true); file->close(); response = new eos::common::PlainHttpResponse(); XrdOucErrInfo error(mVirtualIdentity->tident.c_str()); if (!gOFS->stat(url.c_str(), &buf, error, &etag, &client, "")) { response->AddHeader("ETag", etag); response->AddHeader("Last-Modified", eos::common::Timing::utctime(buf.st_mtime)); } response->SetBody(result); } // clean up the object delete file; } } return response; } /*----------------------------------------------------------------------------*/ eos::common::HttpResponse* HttpHandler::Head(eos::common::HttpRequest* request) { eos::common::HttpResponse* response = Get(request, true); response->mUseFileReaderCallback = false; response->SetBody(""); return response; } /*----------------------------------------------------------------------------*/ eos::common::HttpResponse* HttpHandler::Post(eos::common::HttpRequest* request) { using namespace eos::common; std::string url = request->GetUrl(); eos_static_info("method=POST error=NOTIMPLEMENTED path=%s", url.c_str()); HttpResponse* response = new PlainHttpResponse(); response->SetResponseCode(HttpResponse::ResponseCodes::NOT_IMPLEMENTED); return response; } /*----------------------------------------------------------------------------*/ eos::common::HttpResponse* HttpHandler::Put(eos::common::HttpRequest* request) { XrdSecEntity client(mVirtualIdentity->prot.c_str()); client.name = const_cast(mVirtualIdentity->name.c_str()); client.host = const_cast(mVirtualIdentity->host.c_str()); client.tident = const_cast(mVirtualIdentity->tident.c_str()); std::string url = request->GetUrl(); eos_static_info("method=PUT path=%s", url.c_str()); // Classify path to split between directory or file objects bool isfile = true; bool isOcChunked = false; bool isPartialPut = false; std::map ocHeader; eos::common::HttpResponse* response = 0; XrdOucString spath = request->GetUrl().c_str(); if (!spath.beginswith("/proc/")) { if (spath.endswith("/")) { isfile = false; } } if (eos::common::OwnCloud::isChunkUpload(request)) { isOcChunked = true; // we have to rewrite the path and add some additional header describing // the chunking which was stored in the name url = eos::common::OwnCloud::prepareChunkUpload(request, &response, ocHeader); if (response) { return response; } } if (request->GetHeaders().count("x-upload-range")) { // this is a partial put, we have to remove the truncate flag isPartialPut = true; } std::string etag; { // retrieve the ETag if existing .. struct stat buf; XrdOucErrInfo error(mVirtualIdentity->tident.c_str()); if (gOFS->stat(url.c_str(), &buf, error, &etag, &client, "")) { etag = "undef"; } } if ((etag != "undef") && (request->GetHeaders().count("if-match") && (etag != request->GetHeaders()["if-match"]))) { // ETag mismatch eos_static_info("method=PUT error=precondition-failed path=%s etag=%s cond=match r-etag=%s", url.c_str(), etag.c_str(), request->GetHeaders()["if-match"].c_str()); response = HttpServer::HttpError("ETag precondition failed", response->PRECONDITION_FAILED); return response; } if ((etag != "undef" && (request->GetHeaders().count("if-non-match") && (etag == request->GetHeaders()["if-non-match"])))) { // ETag match eos_static_info("method=PUT error=precondition-failed path=%s etag=%s cond=not-match r-etag=%s", url.c_str(), etag.c_str(), request->GetHeaders()["if-not-match"].c_str()); response = HttpServer::HttpError("ETag is not modified", response->NOT_MODIFIED); return response; } if (isfile) { XrdSfsFile* file = gOFS->newFile((char*) mVirtualIdentity->tident.c_str()); if (file) { XrdSfsFileOpenMode open_mode = 0; mode_t create_mode = 0; // use the proper creation/open flags for PUT's if (!isPartialPut) { open_mode |= SFS_O_TRUNC; } open_mode |= SFS_O_RDWR; create_mode |= (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); std::string query = request->GetQuery(); if (request->GetHeaders().count("content-length")) { query += "&eos.bookingsize="; //or OC chunked uploads we book the full size const char* oclength = eos::common::OwnCloud::getContentSize(request); if (oclength) { query += oclength; } else { query += request->GetHeaders()["content-length"]; } if (!isOcChunked && !isPartialPut) { query += "&eos.targetsize="; query += request->GetHeaders()["content-length"]; } } else { query = "eos.bookingsize=0"; } if (request->GetHeaders().count("x-oc-mtime")) { // there is an X-OC-Mtime header to force the mtime for that file query += "&eos.mtime="; query += request->GetHeaders()["x-oc-mtime"]; } if (request->GetHeaders().count("x-upload-mtime")) { // there is an x-upload-mtime header to force the mtime for that file query += "&eos.mtime="; query += request->GetHeaders()["x-upload-mtime"]; } if (isOcChunked) { // add the OC opaque information query += eos::common::OwnCloud::HeaderToQuery(ocHeader).c_str(); } // ----------------------------------------------------------- // OC clients are switched automatically to atomic upload mode // ----------------------------------------------------------- if (request->GetHeaders().count("oc-total-length") || isOcChunked) { if (query.length()) { query += "&"; } query += "eos.atomic=1"; } if (isOcChunked) { if (etag != "undef") { // file exists already eos_static_info("removing truncation flag "); //open_mode ^= SFS_O_TRUNC; } } int rc = file->open(url.c_str(), open_mode, create_mode, &client, query.c_str()); if (rc != SFS_OK) { if ((rc != SFS_REDIRECT) && open_mode && (file->error.getErrInfo() == ENOENT)) { // retry as a file creation open_mode |= SFS_O_CREAT; open_mode |= SFS_O_TRUNC; rc = file->open(url.c_str(), open_mode, create_mode, &client, query.c_str()); } } if (rc != SFS_OK) { if (rc == SFS_REDIRECT) { std::string redirection_cgi = file->error.getErrText(); if (file->error.getErrInfo() == 1094) { // MGM redirect response = HttpServer::HttpRedirect(request->GetUrl(), redirection_cgi, gOFS->mHttpdPort, false); } else { if (isOcChunked) { redirection_cgi += eos::common::OwnCloud::HeaderToQuery(ocHeader).c_str(); } // FST redirect response = HttpServer::HttpRedirect(request->GetUrl(), redirection_cgi, file->error.getErrInfo(), false); } } else if (rc == SFS_ERROR) { if (file->error.getErrInfo() == ENOENT) { response = HttpServer::HttpError(file->error.getErrText(), 409); } else response = HttpServer::HttpError(file->error.getErrText(), file->error.getErrInfo()); } else if (rc == SFS_DATA) { response = HttpServer::HttpData(file->error.getErrText(), file->error.getErrInfo()); } else if (rc == SFS_STALL) { response = HttpServer::HttpStall(file->error.getErrText(), file->error.getErrInfo()); } else { response = HttpServer::HttpError("Unexpected result from file open", EOPNOTSUPP); } } else { response = new eos::common::PlainHttpResponse(); response->SetResponseCode(response->CREATED); } std::string rurl = file->error.getErrText(); rurl.erase(0, rurl.find('?') + 1); XrdOucEnv env(rurl.c_str()); char* etag = env.Get("mgm.etag"); if (etag) { // add the ETag into the header response->AddHeader("ETag", etag); } // clean up the object delete file; } } else { // DIR requests response = HttpServer::HttpError("Not Implemented", EOPNOTSUPP); } return response; } /*----------------------------------------------------------------------------*/ eos::common::HttpResponse* HttpHandler::Delete(eos::common::HttpRequest* request) { eos::common::HttpResponse* response = 0; XrdOucErrInfo error(mVirtualIdentity->tident.c_str()); struct stat buf; ProcCommand cmd; std::string url = request->GetUrl(); eos_static_info("method=DELETE path=%s", url.c_str()); if (gOFS->_stat(request->GetUrl().c_str(), &buf, error, *mVirtualIdentity, "")) { response = HttpServer::HttpError(error.getErrText(), response->NOT_FOUND); return response; } XrdOucString info = "mgm.cmd=rm&mgm.path="; info += request->GetUrl().c_str(); if (S_ISDIR(buf.st_mode)) { info += "&mgm.option=r"; } cmd.open("/proc/user", info.c_str(), *mVirtualIdentity, &error); cmd.close(); int rc = cmd.GetRetc(); if (rc != SFS_OK) { if (error.getErrInfo() == EPERM) { response = HttpServer::HttpError(error.getErrText(), response->FORBIDDEN); } else if (error.getErrInfo() == ENOENT) { response = HttpServer::HttpError(error.getErrText(), response->NOT_FOUND); } else { response = HttpServer::HttpError(error.getErrText(), error.getErrInfo()); } } else { response = new eos::common::PlainHttpResponse(); response->SetResponseCode(response->NO_CONTENT); } return response; } /*----------------------------------------------------------------------------*/ eos::common::HttpResponse* HttpHandler::Trace(eos::common::HttpRequest* request) { using namespace eos::common; std::string url = request->GetUrl(); eos_static_info("method=TRACE error=NOTIMPLEMENTED path=%s", url.c_str()); HttpResponse* response = new PlainHttpResponse(); response->SetResponseCode(HttpResponse::ResponseCodes::NOT_IMPLEMENTED); return response; } /*----------------------------------------------------------------------------*/ eos::common::HttpResponse* HttpHandler::Options(eos::common::HttpRequest* request) { eos::common::HttpResponse* response = new eos::common::PlainHttpResponse(); response->AddHeader("DAV", "1,2"); response->AddHeader("Allow", "OPTIONS,GET,HEAD,PUT,DELETE,TRACE,"\ "PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK"); response->AddHeader("Content-Length", "0"); return response; } /*----------------------------------------------------------------------------*/ eos::common::HttpResponse* HttpHandler::Connect(eos::common::HttpRequest* request) { using namespace eos::common; std::string url = request->GetUrl(); eos_static_info("method=CONNECT error=NOTIMPLEMENTED path=%s", url.c_str()); HttpResponse* response = new PlainHttpResponse(); response->SetResponseCode(HttpResponse::ResponseCodes::NOT_IMPLEMENTED); return response; } /*----------------------------------------------------------------------------*/ eos::common::HttpResponse* HttpHandler::Patch(eos::common::HttpRequest* request) { using namespace eos::common; std::string url = request->GetUrl(); eos_static_info("method=PATCH error=NOTIMPLEMENTED path=%s", url.c_str()); HttpResponse* response = new PlainHttpResponse(); response->SetResponseCode(HttpResponse::ResponseCodes::NOT_IMPLEMENTED); return response; } /*----------------------------------------------------------------------------*/ EOSMGMNAMESPACE_END