//------------------------------------------------------------------------------ //! @file ProcCommand.cc //------------------------------------------------------------------------------ /************************************************************************ * EOS - the CERN Disk Storage System * * Copyright (C) 2017 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 "ProcCommand.hh" #include "common/Path.hh" #include "mgm/XrdMgmOfs.hh" #include "common/CommentLog.hh" #include "XrdOuc/XrdOucTokenizer.hh" #include "XrdOuc/XrdOucEnv.hh" #include "namespace/interface/IView.hh" #include "namespace/interface/IFileMDSvc.hh" #include "json/json.h" EOSMGMNAMESPACE_BEGIN //------------------------------------------------------------------------------ // Constructor //------------------------------------------------------------------------------ ProcCommand::ProcCommand(): pVid(0), mPath(""), mCmd(""), mSubCmd(""), mArgs(""), mResultStream(""), pOpaque(0), ininfo(0), mDoSort(false), mSelection(0), mOutFormat(""), mOutDepth(0), fstdout(0), fstderr(0), fresultStream(0), fstdoutfilename(""), fstderrfilename(""), fresultStreamfilename(""), mError(0), mLen(0), mAdminCmd(false), mUserCmd(false), mFuseFormat(false), mJsonFormat(false), mHttpFormat(false), mClosed(false), mSendRetc(false), mJsonCallback("") {} //------------------------------------------------------------------------------ // Constructor with parameter //------------------------------------------------------------------------------ ProcCommand::ProcCommand(eos::common::VirtualIdentity& vid): ProcCommand() { pVid = &vid; } //------------------------------------------------------------------------------ // Destructor //------------------------------------------------------------------------------ ProcCommand::~ProcCommand() { if (fstdout) { fclose(fstdout); fstdout = 0; unlink(fstdoutfilename.c_str()); } if (fstderr) { fclose(fstderr); fstderr = 0; unlink(fstderrfilename.c_str()); } if (fresultStream) { fclose(fresultStream); fresultStream = 0; unlink(fresultStreamfilename.c_str()); } if (pOpaque) { delete pOpaque; pOpaque = 0; } } //------------------------------------------------------------------------------ // Open temporary output files for results of find commands //------------------------------------------------------------------------------ bool ProcCommand::OpenTemporaryOutputFiles() { char tmpdir [4096]; snprintf(tmpdir, sizeof(tmpdir) - 1, "%s/%llu", gOFS->TmpStorePath.c_str(), (unsigned long long) XrdSysThread::ID()); fstdoutfilename = tmpdir; fstdoutfilename += ".stdout"; fstderrfilename = tmpdir; fstderrfilename += ".stderr"; fresultStreamfilename = tmpdir; fresultStreamfilename += ".mResultstream"; eos::common::Path cPath(fstdoutfilename.c_str()); if (!cPath.MakeParentPath(S_IRWXU)) { eos_err("Unable to create temporary outputfile directory %s", tmpdir); return false; } // own the directory by daemon if (::chown(cPath.GetParentPath(), 2, 2)) { eos_err("Unable to own temporary outputfile directory %s", cPath.GetParentPath()); } fstdout = fopen(fstdoutfilename.c_str(), "w"); fstderr = fopen(fstderrfilename.c_str(), "w"); fresultStream = fopen(fresultStreamfilename.c_str(), "w+"); if ((!fstdout) || (!fstderr) || (!fresultStream)) { if (fstdout) { fclose(fstdout); } if (fstderr) { fclose(fstderr); } if (fresultStream) { fclose(fresultStream); } return false; } return true; } //------------------------------------------------------------------------------ // Open a proc command e.g. call the appropriate user or admin command and // store the output in a resultstream of in case of find in temporary output // files. //------------------------------------------------------------------------------ int ProcCommand::open(const char* inpath, const char* info, eos::common::VirtualIdentity& vid_in, XrdOucErrInfo* error) { pVid = &vid_in; mClosed = false; mPath = inpath; mDoSort = false; mError = error; ininfo = info; if ((mPath.beginswith("/proc/admin"))) { mAdminCmd = true; } if (mPath.beginswith("/proc/user")) { mUserCmd = true; } // Deal with '&' ... sigh XrdOucString sinfo = ininfo; for (int i = 0; i < sinfo.length(); i++) { if (sinfo[i] == '&') { // figure out if this is a real separator or XrdOucString follow = sinfo.c_str() + i + 1; if (!follow.beginswith("mgm.") && (!follow.beginswith("eos.")) && (!follow.beginswith("xrd.")) && (!follow.beginswith("callback")) && (!follow.beginswith("authz"))) { sinfo.erase(i, 1); sinfo.insert("#AND#", i); } } } pOpaque = new XrdOucEnv(sinfo.c_str()); if (!pOpaque) { // alloc failed return SFS_ERROR; } mOutFormat = ""; mOutDepth = 0; mCmd = pOpaque->Get("mgm.cmd"); mSubCmd = pOpaque->Get("mgm.subcmd"); mOutFormat = pOpaque->Get("mgm.outformat"); long depth = pOpaque->GetInt("mgm.outdepth"); if (depth > 0) { mOutDepth = (unsigned)depth; } mSelection = pOpaque->Get("mgm.selection"); mComment = pOpaque->Get("mgm.comment") ? pOpaque->Get("mgm.comment") : ""; mJsonCallback = pOpaque->Get("callback") ? pOpaque->Get("callback") : ""; mSendRetc = pOpaque->Get("mgm.retc") ? true : false; eos_static_debug("json-callback=%s opaque=%s", mJsonCallback.c_str(), sinfo.c_str()); int envlen = 0; mArgs = pOpaque->Env(envlen); mFuseFormat = false; mJsonFormat = false; mHttpFormat = false; // If set to FUSE, don't print the stdout,stderr tags and we guarantee a line // feed in the end XrdOucString format = pOpaque->Get("mgm.format"); if (format == "fuse") { mFuseFormat = true; } if (format == "json") { mJsonFormat = true; } if (format == "http") { mHttpFormat = true; } stdOut = ""; stdErr = ""; retc = 0; mResultStream = ""; mLen = 0; mDoSort = true; if (mJsonCallback.length()) { mJsonFormat = true; } // Admin command section if (mAdminCmd) { if (mCmd == "archive") { Archive(); mDoSort = false; } else if (mCmd == "backup") { Backup(); mDoSort = false; } else if (mCmd == "geosched") { GeoSched(); mDoSort = false; } else if (mCmd == "fusex") { Fusex(); mDoSort = false; } else if (mCmd == "vid") { Vid(); } else if (mCmd == "rtlog") { Rtlog(); mDoSort = false; } else if (mCmd == "access") { // @todo (faluchet) drop when move to 5.0.0 Access(); mDoSort = false; } else if (mCmd == "config") { // @todo (faluchet) drop when move to 5.0.0 Config(); mDoSort = false; } else if (mCmd == "group") { // @todo (faluchet) drop when move to 5.0.0 Group(); mDoSort = false; } else if (mCmd == "quota") { // @todo (faluchet) drop when move to 5.0.0 AdminQuota(); mDoSort = false; } else { // command is not implemented stdErr += "error: no such admin command '"; stdErr += mCmd; stdErr += "'"; retc = EINVAL; } MakeResult(); return SFS_OK; } // User command section if (mUserCmd) { if (mCmd == "accounting") { Accounting(); mDoSort = false; } else if (mCmd == "archive") { Archive(); mDoSort = false; } else if (mCmd == "motd") { Motd(); mDoSort = false; } else if (mCmd == "version") { Version(); mDoSort = false; } else if (mCmd == "who") { Who(); mDoSort = false; } else if (mCmd == "fuse") { return Fuse(); } else if (mCmd == "fuseX") { return FuseX(); } else if (mCmd == "file") { File(); mDoSort = false; } else if (mCmd == "fileinfo") { Fileinfo(); mDoSort = false; } else if (mCmd == "mkdir") { Mkdir(); } else if (mCmd == "rmdir") { Rmdir(); } else if (mCmd == "cd") { Cd(); mDoSort = false; } else if (mCmd == "chown") { Chown(); } else if (mCmd == "ls") { Ls(); mDoSort = false; } else if (mCmd == "rm") { Rm(); } else if (mCmd == "whoami") { Whoami(); mDoSort = false; } else if (mCmd == "find") { Find(); } else if (mCmd == "map") { Map(); } else if (mCmd == "member") { Member(); } else if (mCmd == "attr") { Attr(); mDoSort = false; } else if (mCmd == "chmod") { Chmod(); } else if (mCmd == "quota") { // @todo (faluchet) drop when move to 5.0.0 UserQuota(); mDoSort = false; } else { // Command not implemented stdErr += "error: no such user command '"; stdErr += mCmd; stdErr += "'"; retc = ENOTSUP; } if (mSendRetc) { // client wants return code on open if (retc) return gOFS->Emsg((const char*) "open", *error, retc, "execute command", ininfo); else { return SFS_OK; } } else { // client gets result stream MakeResult(); return SFS_OK; } } // If neither admin nor proc command return gOFS->Emsg((const char*) "open", *error, EINVAL, "execute command - not implemented ", ininfo); } //------------------------------------------------------------------------------ // Read a part of the result stream produced during open //------------------------------------------------------------------------------ size_t ProcCommand::read(XrdSfsFileOffset boff, char* buff, XrdSfsXferSize blen) { if (fresultStream) { // file based results go here ... if ((fseek(fresultStream, boff, 0)) == 0) { size_t nread = fread(buff, 1, blen, fresultStream); if (nread > 0) { return nread; } } else { eos_err("seek to %llu failed\n", boff); } return 0; } else { if (mLen - boff <= 0) { return 0; } // Memory based results go here ... if (((unsigned int) blen <= (mLen - boff))) { memcpy(buff, mResultStream.c_str() + boff, blen); return blen; } else { memcpy(buff, mResultStream.c_str() + boff, (mLen - boff)); return (mLen - boff); } } } //------------------------------------------------------------------------------ // Return stat information for the result stream to tell the client the size // of the proc output. //------------------------------------------------------------------------------ int ProcCommand::stat(struct stat* buf) { memset(buf, 0, sizeof(struct stat)); buf->st_size = mLen; return SFS_OK; } //------------------------------------------------------------------------------ // Close the proc stream and store the client's command comment // in the comments logbook. //------------------------------------------------------------------------------ int ProcCommand::close() { if (!mClosed) { // Only instance users or sudoers can add to the logbook if ((pVid->uid <= 2) || (pVid->sudoer)) { if (mComment.length() && gOFS->mCommentLog) { if (!gOFS->mCommentLog->Add(mTimestamp, mCmd.c_str(), mSubCmd.c_str(), mArgs.c_str(), mComment.c_str(), stdErr.c_str(), retc)) { eos_err("failed to log to comments logbook"); } } } mClosed = true; } return retc; } //------------------------------------------------------------------------------ // Build the in-memory result of the stdout, stderr & retc of the proc command. // Depending on the output format the key-value CGI returned changes => see // implementation. //------------------------------------------------------------------------------ void ProcCommand::MakeResult() { using eos::common::StringConversion; mResultStream = ""; if (!fstdout) { if (mDoSort) { eos::common::StringConversion::SortLines(stdOut); } if ((!mFuseFormat && !mJsonFormat && !mHttpFormat)) { // The default format mResultStream = "mgm.proc.stdout="; mResultStream += StringConversion::Seal(stdOut); mResultStream += "&mgm.proc.stderr="; mResultStream += StringConversion::Seal(stdErr); mResultStream += "&mgm.proc.retc="; mResultStream += std::to_string(retc); } if (mFuseFormat || mHttpFormat) { if (mFuseFormat) { mResultStream += stdOut.c_str(); } else { mResultStream += "\n"; mResultStream += "\n"; mResultStream += "EOS-HTTP \n"; mResultStream += " \n"; // block cross-site scripting in responses if (stdErr.length()) { mResultStream += "\n"; } mResultStream += "
\n"; // FUSE format contains only STDOUT if (stdOut.length() && KeyValToHttpTable(stdOut)) { mResultStream += stdOut.c_str(); } else { if (stdErr.length() || retc) { mResultStream += stdOut.c_str(); mResultStream += "

⚠ "; mResultStream += stdErr.c_str(); mResultStream += "

"; } else { if (!stdOut.length()) { mResultStream += "

✔ "; mResultStream += "Success!"; mResultStream += "

"; } else { mResultStream += stdOut.c_str(); } } } mResultStream += "
"; } } if (mJsonFormat) { if (!stdJson.length()) { Json::Value json; try { Json::Value jsonOut; json["errormsg"] = stdErr.c_str(); json["retc"] = std::to_string(retc); jsonOut = IProcCommand::ConvertOutputToJsonFormat(stdOut.c_str()); if (mCmd.length()) { if (mSubCmd.length()) { json[mCmd.c_str()][mSubCmd.c_str()] = jsonOut; } else { json[mCmd.c_str()] = jsonOut; } } else { json["result"] = jsonOut; } } catch (Json::Exception& e) { eos_static_err("Json conversion exception cmd=%s subcmd=%s " "emsg=\"%s\"", mCmd.c_str(), mSubCmd.c_str(), e.what()); json["errormsg"] = "illegal string in json conversion"; json["retc"] = std::to_string(EFAULT); } stdJson = SSTR(json).c_str(); } if (mJsonCallback.length()) { // JSONP mResultStream = mJsonCallback.c_str(); mResultStream += "([\n"; mResultStream += stdJson.c_str(); mResultStream += "\n]);"; } else { // JSON if (vid.prot.beginswith("http")) { mResultStream = stdJson.c_str(); } else { mResultStream = "mgm.proc.json="; mResultStream += StringConversion::Seal(stdJson); } } } if (mResultStream.length() && (*(mResultStream.rbegin()) != '\n')) { mResultStream += "\n"; } if (retc) { eos_static_err("%s (errno=%u)", stdErr.c_str(), retc); } mLen = mResultStream.length(); } else { // File based results CANNOT be sorted and don't have mFuseFormat if (!mFuseFormat) { // Create the stdout result if (!fseek(fstdout, 0, 0) && !fseek(fstderr, 0, 0) && !fseek(fresultStream, 0, 0)) { fprintf(fresultStream, "&mgm.proc.stdout="); std::ifstream inStdout(fstdoutfilename.c_str()); std::ifstream inStderr(fstderrfilename.c_str()); std::string entry; while (std::getline(inStdout, entry)) { XrdOucString sentry = entry.c_str(); sentry += "\n"; if (!mFuseFormat) { StringConversion::Seal(sentry); } fprintf(fresultStream, "%s", sentry.c_str()); } // Close and remove - if this fails there is nothing to recover anyway fclose(fstdout); fstdout = 0; unlink(fstdoutfilename.c_str()); // Create the stderr result fprintf(fresultStream, "&mgm.proc.stderr="); while (std::getline(inStderr, entry)) { XrdOucString sentry = entry.c_str(); sentry += "\n"; StringConversion::Seal(sentry); fprintf(fresultStream, "%s", sentry.c_str()); } // Close and remove - if this fails there is nothing to recover anyway fclose(fstderr); fstderr = 0; unlink(fstderrfilename.c_str()); fprintf(fresultStream, "&mgm.proc.retc=%d", retc); mLen = ftell(fresultStream); // Spool the resultstream to the beginning fseek(fresultStream, 0, 0); } else { eos_static_err("cannot seek to position 0 in result files"); } } } } //------------------------------------------------------------------------------ // Try to detect and convert a monitor output format and convert it into a // nice http table //------------------------------------------------------------------------------ bool ProcCommand::KeyValToHttpTable(XrdOucString& stdOut) { while (stdOut.replace("= ", "=\"\"")) { } std::string stmp = stdOut.c_str(); XrdOucTokenizer tokenizer((char*) stmp.c_str()); const char* line; bool ok = true; std::vector keys; std::vector < std::map < std::string, std::string >> keyvaluetable; std::string table; while ((line = tokenizer.GetLine())) { if (strlen(line) <= 1) { continue; } std::map keyval; if (eos::common::StringConversion::GetKeyValueMap(line, keyval, "=", " ", &keys)) { keyvaluetable.push_back(keyval); } else { ok = false; break; } } if (ok) { table += R"literal( )literal"; table += "\n"; // build the header table += "\n"; for (size_t i = 0; i < keys.size(); i++) { table += ""; table += "\n"; } table += "\n"; // build the rows for (size_t i = 0; i < keyvaluetable.size(); i++) { table += "\n"; for (size_t j = 0; j < keys.size(); j++) { table += ""; } table += "\n"; table += "\n"; } table += "
"; table += ""; // for keys don't print lengthy strings like a.b.c.d ... just print d std::string dotkeys = keys[i]; size_t pos = dotkeys.rfind("."); if (pos != std::string::npos) dotkeys.erase(0, pos + 1); //table += dotkeys; table += keys[i]; table += ""; table += "
"; table += ""; XrdOucString sizestring = keyvaluetable[i][keys[j]].c_str(); unsigned long long val = eos::common::StringConversion::GetSizeFromString(sizestring); if (errno || val == 0 || (!sizestring.isdigit())) { XrdOucString decodeURI = keyvaluetable[i][keys[j]].c_str(); // we need to remove URI encoded spaces now while (decodeURI.replace("%20", " ")) { } table += decodeURI.c_str(); } else { eos::common::StringConversion::GetReadableSizeString(sizestring, val, ""); table += sizestring.c_str(); } table += ""; table += "
\n"; stdOut = table.c_str(); } return ok; } Json::Value ProcCommand::CallJsonFormatter(const std::string& output) { return IProcCommand::ConvertOutputToJsonFormat(output); } EOSMGMNAMESPACE_END