관리-도구
편집 파일: AdminPanelConnector.h
/* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2017-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_ADMIN_PANEL_CONNECTOR_H_ #define _PASSENGER_ADMIN_PANEL_CONNECTOR_H_ #include <sys/wait.h> #include <sstream> #include <unistd.h> #include <boost/scoped_ptr.hpp> #include <boost/thread.hpp> #include <boost/bind/bind.hpp> #include <boost/foreach.hpp> #include <limits> #include <string> #include <vector> #include <Constants.h> #include <WebSocketCommandReverseServer.h> #include <InstanceDirectory.h> #include <ConfigKit/SchemaUtils.h> #include <Core/ApplicationPool/Pool.h> #include <Core/Controller.h> #include <ProcessManagement/Ruby.h> #include <FileTools/FileManip.h> #include <SystemTools/UserDatabase.h> #include <Utils.h> #include <StrIntTools/StrIntUtils.h> #include <IOTools/IOUtils.h> #include <Utils/AsyncSignalSafeUtils.h> #include <LoggingKit/Context.h> #include <jsoncpp/json.h> namespace Passenger { namespace Core { using namespace std; using namespace oxt; namespace ASSU = AsyncSignalSafeUtils; class AdminPanelConnector { public: /** * BEGIN ConfigKit schema: Passenger::Core::AdminPanelConnector::Schema * (do not edit: following text is automatically generated * by 'rake configkit_schemas_inline_comments') * * auth_type string - default("basic") * close_timeout float - default(10.0) * connect_timeout float - default(30.0) * data_debug boolean - default(false) * instance_dir string - read_only * integration_mode string - default("standalone") * log_prefix string - - * password string - secret * password_file string - - * ping_interval float - default(30.0) * ping_timeout float - default(30.0) * proxy_password string - secret * proxy_timeout float - default(30.0) * proxy_url string - - * proxy_username string - - * reconnect_timeout float - default(5.0) * ruby string - default("ruby") * standalone_engine string - default * url string required - * username string - - * web_server_module_version string - read_only * web_server_version string - read_only * websocketpp_debug_access boolean - default(false) * websocketpp_debug_error boolean - default(false) * * END */ struct Schema: public WebSocketCommandReverseServer::Schema { Schema() : WebSocketCommandReverseServer::Schema(false) { using namespace ConfigKit; add("integration_mode", STRING_TYPE, OPTIONAL, DEFAULT_INTEGRATION_MODE); addWithDynamicDefault("standalone_engine", STRING_TYPE, OPTIONAL, ConfigKit::getDefaultStandaloneEngine); add("instance_dir", STRING_TYPE, OPTIONAL | READ_ONLY); add("web_server_version", STRING_TYPE, OPTIONAL | READ_ONLY); add("web_server_module_version", STRING_TYPE, OPTIONAL | READ_ONLY); add("ruby", STRING_TYPE, OPTIONAL, "ruby"); addValidator(ConfigKit::validateIntegrationMode); addValidator(ConfigKit::validateStandaloneEngine); finalize(); } }; typedef WebSocketCommandReverseServer::ConfigChangeRequest ConfigChangeRequest; typedef WebSocketCommandReverseServer::ConnectionPtr ConnectionPtr; typedef WebSocketCommandReverseServer::MessagePtr MessagePtr; typedef boost::function<Json::Value (void)> ConfigGetter; typedef vector<Controller*> Controllers; private: WebSocketCommandReverseServer server; dynamic_thread_group threads; Json::Value globalPropertiesFromInstanceDir; bool onMessage(WebSocketCommandReverseServer *server, const ConnectionPtr &conn, const MessagePtr &msg) { Json::Value doc; try { doc = parseAndBasicValidateMessageAsJSON(msg->get_payload()); } catch (const RuntimeException &e) { Json::Value reply; reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = e.what(); sendJsonReply(conn, reply); return true; } if (doc["action"] == "get") { return onGetMessage(conn, doc); } else { return onUnknownMessageAction(conn, doc); } } bool onGetMessage(const ConnectionPtr &conn, const Json::Value &doc) { const string resource = doc["resource"].asString(); if (resource == "server_properties") { return onGetServerProperties(conn, doc); } else if (resource == "global_properties") { return onGetGlobalProperties(conn, doc); } else if (resource == "global_configuration") { return onGetGlobalConfiguration(conn, doc); } else if (resource == "global_statistics") { return onGetGlobalStatistics(conn, doc); } else if (resource == "application_properties") { return onGetApplicationProperties(conn, doc); } else if (resource == "application_configuration") { return onGetApplicationConfig(conn, doc); } else if (resource == "application_logs") { return onGetApplicationLogs(conn, doc); } else { return onUnknownResource(conn, doc); } } bool onGetServerProperties(const ConnectionPtr &conn, const Json::Value &doc) { threads.create_thread( boost::bind(&AdminPanelConnector::onGetServerPropertiesBgJob, this, conn, doc, server.getConfig()["ruby"].asString()), "AdminPanelCommandServer: get_server_properties background job", 128 * 1024); return false; } void onGetServerPropertiesBgJob(const ConnectionPtr &conn, const Json::Value &doc, const string &ruby) { vector<string> args; args.push_back("passenger-config"); args.push_back("system-properties"); int status = 0; SubprocessOutput output; try { runInternalRubyTool(*resourceLocator, ruby, args, &status, &output); } catch (const std::exception &e) { server.getIoService().post(boost::bind( &AdminPanelConnector::onGetServerPropertiesDone, this, conn, doc, string(), -1, e.what() )); return; } server.getIoService().post(boost::bind( &AdminPanelConnector::onGetServerPropertiesDone, this, conn, doc, output.data, status, string() )); } void onGetServerPropertiesDone(const ConnectionPtr &conn, const Json::Value &doc, const string output, int status, const string &error) { Json::Value reply; reply["request_id"] = doc["request_id"]; if (error.empty()) { if (status == 0 || status == -1) { Json::Reader reader; Json::Value dataDoc; if (output.empty()) { reply["result"] = "error"; reply["data"]["message"] = "Error parsing internal helper tool output"; P_ERROR(getLogPrefix() << "Error parsing internal helper tool output.\n" << "Raw data: \"\""); } else if (reader.parse(output, dataDoc)) { reply["result"] = "ok"; reply["data"] = dataDoc; } else { reply["result"] = "error"; reply["data"]["message"] = "Error parsing internal helper tool output"; P_ERROR(getLogPrefix() << "Error parsing internal helper tool output.\n" << "Error: " << reader.getFormattedErrorMessages() << "\n" "Raw data: \"" << cEscapeString(output) << "\""); } } else { int exitStatus = WEXITSTATUS(status); reply["result"] = "error"; reply["data"]["message"] = "Internal helper tool exited with status " + toString(exitStatus); P_ERROR(getLogPrefix() << "Internal helper tool exited with status " << exitStatus << ". Raw output: \"" << cEscapeString(output) << "\""); } } else { reply["result"] = "error"; reply["data"]["message"] = error; } sendJsonReply(conn, reply); server.doneReplying(conn); } bool onGetGlobalProperties(const ConnectionPtr &conn, const Json::Value &doc) { const ConfigKit::Store &config = server.getConfig(); Json::Value reply, data; reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; data = globalPropertiesFromInstanceDir; data["version"] = PASSENGER_VERSION; data["core_pid"] = Json::UInt(getpid()); string integrationMode = config["integration_mode"].asString(); data["integration_mode"]["name"] = integrationMode; if (!config["web_server_module_version"].isNull()) { data["integration_mode"]["web_server_module_version"] = config["web_server_module_version"]; } if (integrationMode == "standalone") { data["integration_mode"]["standalone_engine"] = config["standalone_engine"]; } if (!config["web_server_version"].isNull()) { data["integration_mode"]["web_server_version"] = config["web_server_version"]; } data["originally_packaged"] = resourceLocator->isOriginallyPackaged(); if (!resourceLocator->isOriginallyPackaged()) { data["packaging_method"] = resourceLocator->getPackagingMethod(); } reply["data"] = data; sendJsonReply(conn, reply); return true; } bool onGetGlobalConfiguration(const ConnectionPtr &conn, const Json::Value &doc) { threads.create_thread( boost::bind(&AdminPanelConnector::onGetGlobalConfigurationBgJob, this, conn, doc), "AdminPanelCommandServer: get_global_config background job", 128 * 1024); return false; } void onGetGlobalConfigurationBgJob(const ConnectionPtr &conn, const Json::Value &input) { Json::Value globalConfig = configGetter()["config_manifest"]["effective_value"]["global_configuration"]; server.getIoService().post(boost::bind( &AdminPanelConnector::onGetGlobalConfigDone, this, conn, input, globalConfig )); } void onGetGlobalConfigDone(const ConnectionPtr &conn, const Json::Value &input, Json::Value config) { Json::Value reply; reply["result"] = "ok"; reply["request_id"] = input["request_id"]; reply["data"]["options"] = config; sendJsonReply(conn, reply); server.doneReplying(conn); } bool onGetGlobalStatistics(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value reply; reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = Json::arrayValue; for (unsigned int i = 0; i < controllers.size(); i++) { reply["data"]["message"].append(controllers[i]->inspectStateAsJson()); } sendJsonReply(conn, reply); return true; } bool onGetApplicationProperties(const ConnectionPtr &conn, const Json::Value &doc) { ConfigKit::Schema argumentsSchema = ApplicationPool2::Pool::ToJsonOptions::createSchema(); Json::Value args(Json::objectValue), reply; ApplicationPool2::Pool::ToJsonOptions inspectOptions = ApplicationPool2::Pool::ToJsonOptions::makeAuthorized(); if (doc.isMember("arguments")) { ConfigKit::Store store(argumentsSchema); vector<ConfigKit::Error> errors; if (store.update(doc["arguments"], errors)) { inspectOptions.set(store.inspectEffectiveValues()); } else { reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = "Invalid arguments: " + ConfigKit::toString(errors); sendJsonReply(conn, reply); return true; } } reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; reply["data"]["applications"] = appPool->inspectPropertiesInAdminPanelFormat( inspectOptions); sendJsonReply(conn, reply); return true; } static void modifyEnvironmentVariables(Json::Value &option) { Json::Value::iterator it; for (it = option.begin(); it != option.end(); it++) { Json::Value &suboption = *it; suboption["value"] = suboption["value"].toStyledString(); } } bool onGetApplicationConfig(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value appConfigsContainer = configGetter()["config_manifest"] ["effective_value"]["application_configurations"]; Json::Value appConfigsContainerOutput; Json::Value reply; if (doc.isMember("arguments")) { ConfigKit::Schema argumentsSchema = ApplicationPool2::Pool::ToJsonOptions::createSchema(); ConfigKit::Store store(argumentsSchema); vector<ConfigKit::Error> errors; if (!store.update(doc["arguments"], errors)) { reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = "Invalid arguments: " + ConfigKit::toString(errors); sendJsonReply(conn, reply); return true; } Json::Value allowedApplicationIds = store.inspectEffectiveValues()["application_ids"]; if (allowedApplicationIds.isNull()) { appConfigsContainerOutput = appConfigsContainer; } else { appConfigsContainerOutput = filterJsonObject( appConfigsContainer, allowedApplicationIds); } } else { appConfigsContainerOutput = appConfigsContainer; } reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; reply["data"]["options"] = appConfigsContainerOutput; sendJsonReply(conn, reply); return true; } void addWatchedFiles() { Json::Value appConfigs = configGetter()["config_manifest"]["effective_value"]["application_configurations"]; // As a hack, we look up the watched files config (passenger monitor log file) in the manifest. The manifest // is meant for users, which means that key names depend on the integration mode. In the future when // component configuration more routed through ConfigKit we can get rid of the hack. string integrationMode = server.getConfig()["integration_mode"].asString(); string passengerMonitorLogFile; string passengerAppRoot; if (integrationMode == "apache") { passengerMonitorLogFile = "PassengerMonitorLogFile"; passengerAppRoot = "PassengerAppRoot"; } else { passengerMonitorLogFile = "passenger_monitor_log_file"; passengerAppRoot = "passenger_app_root"; // TODO: this probably doesn't give any results with the builtin engine (not supported in other places either) } foreach (HashedStaticString key, appConfigs.getMemberNames()) { Json::Value files = appConfigs[key]["options"][passengerMonitorLogFile]["value_hierarchy"][0]["value"]; string appRoot = appConfigs[key]["options"][passengerAppRoot]["value_hierarchy"][0]["value"].asString(); pair<uid_t, gid_t> ids; try { ids = appPool->getGroupRunUidAndGids(key); } catch (const RuntimeException &) { files = Json::nullValue; } if (!files.isNull()) { string usernameOrUid = lookupSystemUsernameByUid(ids.first, true); foreach (Json::Value file, files) { string f = file.asString(); string maxLines = toString(LOG_MONITORING_MAX_LINES); Pipe pipe = createPipe(__FILE__, __LINE__); string agentExe = resourceLocator->findSupportBinary(AGENT_EXE); vector<const char *> execArgs; execArgs.push_back(agentExe.c_str()); execArgs.push_back("exec-helper"); if (geteuid() == 0) { execArgs.push_back("--user"); execArgs.push_back(usernameOrUid.c_str()); } execArgs.push_back("tail"); execArgs.push_back("-n"); execArgs.push_back(maxLines.c_str()); execArgs.push_back(f.c_str()); execArgs.push_back(NULL); pid_t pid = syscalls::fork(); if (pid == -1) { int e = errno; throw SystemException("Cannot fork a new process", e); } else if (pid == 0) { chdir(appRoot.c_str()); dup2(pipe.second, STDOUT_FILENO); pipe.first.close(); pipe.second.close(); closeAllFileDescriptors(2); execvp(execArgs[0], const_cast<char * const *>(&execArgs[0])); int e = errno; char buf[256]; char *pos = buf; const char *end = pos + 256; pos = ASSU::appendData(pos, end, "Cannot execute \""); pos = ASSU::appendData(pos, end, agentExe.c_str()); pos = ASSU::appendData(pos, end, "\": "); pos = ASSU::appendData(pos, end, strerror(e)); pos = ASSU::appendData(pos, end, " (errno="); pos = ASSU::appendInteger<int, 10>(pos, end, e); pos = ASSU::appendData(pos, end, ")\n"); ASSU::writeNoWarn(STDERR_FILENO, buf, pos - buf); _exit(1); } else { pipe.second.close(); string out = readAll(pipe.first, std::numeric_limits<size_t>::max()).first; LoggingKit::context->saveMonitoredFileLog(key, f.c_str(), f.size(), out.data(), out.size()); pipe.first.close(); syscalls::waitpid(pid, NULL, 0); } } } } } bool onGetApplicationLogs(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value reply; reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; addWatchedFiles(); reply["data"]["logs"] = LoggingKit::context->convertLog(); sendJsonReply(conn, reply); return true; } bool onUnknownResource(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value reply; reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = "Unknown resource '" + doc["resource"].asString() + "'"; sendJsonReply(conn, reply); return true; } bool onUnknownMessageAction(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value reply; reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = "Unknown action '" + doc["action"].asString() + "'"; sendJsonReply(conn, reply); return true; } Json::Value parseAndBasicValidateMessageAsJSON(const string &msg) const { Json::Value doc; Json::Reader reader; if (!reader.parse(msg, doc)) { throw RuntimeException("Error parsing command JSON document: " + reader.getFormattedErrorMessages()); } if (!doc.isObject()) { throw RuntimeException("Invalid command JSON document: must be an object"); } if (!doc.isMember("action")) { throw RuntimeException("Invalid command JSON document: missing 'action' key"); } if (!doc["action"].isString()) { throw RuntimeException("Invalid command JSON document: the 'action' key must be a string"); } if (!doc.isMember("request_id")) { throw RuntimeException("Invalid command JSON document: missing 'request_id' key"); } if (!doc.isMember("resource")) { throw RuntimeException("Invalid command JSON document: missing 'resource' key"); } if (!doc["resource"].isString()) { throw RuntimeException("Invalid command JSON document: the 'resource' key must be a string"); } if (doc.isMember("arguments") && !doc["arguments"].isObject()) { throw RuntimeException("Invalid command JSON document: the 'arguments' key, when present, must be an object"); } return doc; } void sendJsonReply(const ConnectionPtr &conn, const Json::Value &doc) { Json::FastWriter writer; string str = writer.write(doc); WCRS_DEBUG_FRAME(&server, "Replying with:", str); conn->send(str); } void readInstanceDirProperties(const string &instanceDir) { Json::Value doc; Json::Reader reader; if (!reader.parse(unsafeReadFile(instanceDir + "/properties.json"), doc)) { throw RuntimeException("Cannot parse " + instanceDir + "/properties.json: " + reader.getFormattedErrorMessages()); } globalPropertiesFromInstanceDir["instance_id"] = doc["instance_id"]; globalPropertiesFromInstanceDir["watchdog_pid"] = doc["watchdog_pid"]; } Json::Value filterJsonObject(const Json::Value &object, const Json::Value &allowedKeys) const { Json::Value::const_iterator it, end = allowedKeys.end(); Json::Value result(Json::objectValue); for (it = allowedKeys.begin(); it != end; it++) { if (object.isMember(it->asString())) { result[it->asString()] = object[it->asString()]; } } return result; } void initializePropertiesWithoutInstanceDir() { globalPropertiesFromInstanceDir["instance_id"] = InstanceDirectory::generateInstanceId(); } string getLogPrefix() const { return server.getConfig()["log_prefix"].asString(); } WebSocketCommandReverseServer::MessageHandler createMessageFunctor() { return boost::bind(&AdminPanelConnector::onMessage, this, boost::placeholders::_1, boost::placeholders::_2, boost::placeholders::_3); } public: /******* Dependencies *******/ ResourceLocator *resourceLocator; ApplicationPool2::PoolPtr appPool; ConfigGetter configGetter; Controllers controllers; AdminPanelConnector(const Schema &schema, const Json::Value &config, const ConfigKit::Translator &translator = ConfigKit::DummyTranslator()) : server(schema, createMessageFunctor(), config, translator), resourceLocator(NULL) { if (!config["instance_dir"].isNull()) { readInstanceDirProperties(config["instance_dir"].asString()); } else { initializePropertiesWithoutInstanceDir(); } } void initialize() { if (resourceLocator == NULL) { throw RuntimeException("resourceLocator must be non-NULL"); } if (appPool == NULL) { throw RuntimeException("appPool must be non-NULL"); } if (configGetter.empty()) { throw RuntimeException("configGetter must be non-NULL"); } server.initialize(); } void run() { server.run(); } void asyncPrepareConfigChange(const Json::Value &updates, ConfigChangeRequest &req, const ConfigKit::CallbackTypes<WebSocketCommandReverseServer>::PrepareConfigChange &callback) { server.asyncPrepareConfigChange(updates, req, callback); } void asyncCommitConfigChange(ConfigChangeRequest &req, const ConfigKit::CallbackTypes<WebSocketCommandReverseServer>::CommitConfigChange &callback) BOOST_NOEXCEPT_OR_NOTHROW { server.asyncCommitConfigChange(req, callback); } void asyncShutdown(const WebSocketCommandReverseServer::Callback &callback = WebSocketCommandReverseServer::Callback()) { server.asyncShutdown(callback); } }; } // namespace Core } // namespace Passenger #endif /* _PASSENGER_ADMIN_PANEL_CONNECTOR_H_ */