/* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2010-2017 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_APPLICATION_POOL2_OPTIONS_H_ #define _PASSENGER_APPLICATION_POOL2_OPTIONS_H_ #include #include #include #include #include #include #include #include #include #include #include #include namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /** * This struct encapsulates information for ApplicationPool::get() and for * Spawner::spawn(), such as which application is to be spawned. * * ## Privilege lowering support * * If user is given and isn't the empty string, then the application process * will run as the given username. Otherwise, the owner of the application's startup * file (e.g. config.ru) will be used. * * If group is given and isn't the empty string, then the application process * will run as the given group name. If it's set to the special value * "!STARTUP_FILE!", then the startup file's group will be used. Otherwise, * the primary group of the user that the application process will run as, * will be used as group. * * If the user or group that the application process attempts to switch to * doesn't exist, then defaultUser and defaultGroup, respectively, * will be used. * * Phusion Passenger will attempt to avoid running the application process as * root: if user or group is set to the root user or the root group, * or if the startup file is owned by root, then defaultUser and * defaultGroup will be used instead. * * All this only happen if Phusion Passenger has root privileges. If not, then * these options have no effect. */ class Options { private: shared_array storage; template static vector getStringFields(OptionsClass &options) { vector result; result.reserve(20); result.push_back(&options.appRoot); result.push_back(&options.appGroupName); result.push_back(&options.appType); result.push_back(&options.startCommand); result.push_back(&options.startupFile); result.push_back(&options.processTitle); result.push_back(&options.environment); result.push_back(&options.baseURI); result.push_back(&options.spawnMethod); result.push_back(&options.user); result.push_back(&options.group); result.push_back(&options.defaultUser); result.push_back(&options.defaultGroup); result.push_back(&options.restartDir); result.push_back(&options.preexecChroot); result.push_back(&options.postexecChroot); result.push_back(&options.integrationMode); result.push_back(&options.ruby); result.push_back(&options.python); result.push_back(&options.nodejs); result.push_back(&options.meteorAppSettings); result.push_back(&options.environmentVariables); result.push_back(&options.ustRouterAddress); result.push_back(&options.ustRouterUsername); result.push_back(&options.ustRouterPassword); result.push_back(&options.apiKey); result.push_back(&options.hostName); result.push_back(&options.uri); result.push_back(&options.unionStationKey); return result; } static inline void appendKeyValue(vector &vec, const char *key, const StaticString &value) { if (!value.empty()) { vec.push_back(key); vec.push_back(value.toString()); } } static inline void appendKeyValue(vector &vec, const char *key, const char *value) { vec.push_back(key); vec.push_back(value); } static inline void appendKeyValue2(vector &vec, const char *key, long value) { vec.push_back(key); vec.push_back(toString(value)); } static inline void appendKeyValue3(vector &vec, const char *key, unsigned long value) { vec.push_back(key); vec.push_back(toString(value)); } static inline void appendKeyValue4(vector &vec, const char *key, bool value) { vec.push_back(key); vec.push_back(value ? "true" : "false"); } public: /*********** Spawn options that should be set by the caller *********** * These are the options that are relevant while spawning an application * process. These options are only used during spawning. */ /** * The root directory of the application to spawn. In case of a Ruby on Rails * application, this is the folder that contains 'app/', 'public/', 'config/', * etc. This must be a valid directory, but the path does not have to be absolute. */ HashedStaticString appRoot; /** * A name used by ApplicationPool to uniquely identify an application. * If one tries to get() from the application pool with name "A", then get() * again with name "B", then the latter will spawn a new application process, * even if both get() requests have the same app root. * * If left empty, then the app root is used as the app group name. */ HashedStaticString appGroupName; /** The application's type, used for determining the command to invoke to * spawn an application process as well as determining the startup file's * filename. It can be one of the app type names in AppType.cpp, or the * empty string (default). In case of the latter, 'startCommand' and * 'startupFile' (which MUST be set) will dictate the startup command * and the startup file's filename. */ StaticString appType; /** The command for spawning the application process. This is a list of * arguments, separated by '\t', e.g. "ruby\tfoo.rb". Only used * during spawning and only if appType.empty(). */ StaticString startCommand; /** Filename of the application's startup file. Only actually used for * determining user switching info. Only used during spawning and only * if appType.empty(). */ StaticString startupFile; /** The process title to assign to the application process. Only used * during spawning. May be empty in which case no particular process * title is assigned. Only used during spawning and only if * appType.empty(). */ StaticString processTitle; /** * Defaults to DEFAULT_LOG_LEVEL. */ int logLevel; /** The maximum amount of time, in milliseconds, that may be spent * on spawning the process or the preloader. */ unsigned int startTimeout; /** * The RAILS_ENV/RACK_ENV environment that should be used. May not be an * empty string. */ StaticString environment; /** * The base URI on which the application runs. If the application is * running on the root URI, then this value must be "/". * * @invariant baseURI != "" */ StaticString baseURI; /** * Spawning method, either "smart" or "direct". */ StaticString spawnMethod; /** See overview. */ StaticString user; /** See class overview. */ StaticString group; /** See class overview. Defaults to "nobody". */ StaticString defaultUser; /** See class overview. Defaults to the defaultUser's primary group. */ StaticString defaultGroup; /** Minimum user id starting from which entering LVE and CageFS is allowed. */ unsigned int lveMinUid; /** * The directory which contains restart.txt and always_restart.txt. * An empty string means that the default directory should be used. */ StaticString restartDir; StaticString preexecChroot; StaticString postexecChroot; StaticString integrationMode; /** * Path to the Ruby interpreter to use, in case the application to spawn * is a Ruby app. */ StaticString ruby; /** * Path to the Python interpreter to use, in case the application to spawn * is a Python app. */ StaticString python; /** * Path to the Node.js command to use, in case the application to spawn * is a Node.js app. */ StaticString nodejs; /** * When running meteor in non-bundled mode, settings for the application need to be specified * via --settings (instead of through the METEOR_SETTINGS environment variable), */ StaticString meteorAppSettings; /** * Environment variables which should be passed to the spawned application * process. This is a base64-encoded string of key-value pairs, with each * element terminated by a NUL character. For example: * * base64("PATH\0/usr/bin\0RUBY\0/usr/bin/ruby\0") */ StaticString environmentVariables; unsigned int fileDescriptorUlimit; /** * If set to a value that isn't -1, makes Passenger ignore the application's * advertised socket concurrency, and believe that the concurrency should be * the given value. * * Defaults to -1. */ int forceMaxConcurrentRequestsPerProcess; /** Whether debugger support should be enabled. */ bool debugger; /** Whether to load environment variables set in shell startup * files (e.g. ~/.bashrc) during spawning. */ bool loadShellEnvvars; bool userSwitching; /** Whether Union Station logging should be enabled. Enabling this option will * result in: * * - The application enabling its Union Station support. * - Periodic tasks such as `collectAnalytics()` to log things to Union Station. * * It does *not* necessarily result in a request logging data to Union Station. * That depends on whether the `transaction` member is set. * * If this is set to true, then 'ustRouterAddress', 'ustRouterUsername' * and 'ustRouterPassword' must be non-empty. */ bool analytics; StaticString ustRouterAddress; StaticString ustRouterUsername; StaticString ustRouterPassword; /** * Whether Spawner should raise an internal error when spawning. Used * during unit tests. */ bool raiseInternalError; /*********** Per-group pool options that should be set by the caller *********** * These options dictate how Pool will manage processes, routing, etc. within * a single Group. These options are not process-specific, only group-specific. */ /** * The minimum number of processes for the current group that the application * pool's cleaner thread should keep around. */ unsigned int minProcesses; /** * The maximum number of processes that may be spawned * for this app root. This option only has effect if it's lower than * the pool size. * * A value of 0 means unspecified, and has no effect. */ unsigned int maxProcesses; /** The number of seconds that preloader processes may stay alive idling. */ long maxPreloaderIdleTime; /** * The maximum number of processes inside a group that may be performing * out-of-band work at the same time. */ unsigned int maxOutOfBandWorkInstances; /** * The maximum number of requests that may live in the Group.getWaitlist queue. * A value of 0 means unlimited. */ unsigned int maxRequestQueueSize; /** * Whether websocket connections should be aborted on process shutdown * or restart. */ bool abortWebsocketsOnProcessShutdown; /** * The Union Station key to use in case analytics logging is enabled. * It is used by Pool::collectAnalytics() and other administrative * functions which are called periodically. Because they do not belong * to any request, and they may still want to log to Union Station, * this key is stored in the per-group options structure. * * It is not used on a per-request basis. Per-request analytics logging * (and Union Station logging) uses the logger object in the `logger` field * instead. */ StaticString unionStationKey; /*-----------------*/ /*********** Per-request pool options that should be set by the caller *********** * These options also dictate how Pool will manage processes, etc. Unlike the * per-group options, these options are customizable on a per-request basis. * Their effects also don't persist longer than a single request. */ /** Current request host name. */ StaticString hostName; /** Current request URI. */ StaticString uri; /** * The Union Station log transaction that this request belongs to. * May be the null pointer, in which case Union Station logging is * disabled for this request. * * When an Options object is passed to another thread (either direct or through * a copy), the caller should call `detachFromUnionStationTransaction()`. * Each Union Station transaction object is only supposed to be used in the same * thread. */ UnionStation::TransactionPtr transaction; /** * A sticky session ID for routing to a specific process. */ unsigned int stickySessionId; /** * A throttling rate for file stats. When set to a non-zero value N, * restart.txt and other files which are usually stat()ted on every * ApplicationPool::get() call will be stat()ed at most every N seconds. */ unsigned long statThrottleRate; /** * The maximum number of requests that the spawned application may process * before exiting. A value of 0 means unlimited. */ unsigned long maxRequests; /** If the current time (in microseconds) has already been queried, set it * here. Pool will use this timestamp instead of querying it again. */ unsigned long long currentTime; /** When true, Pool::get() and Pool::asyncGet() will create the necessary * Group structure just as normally, and will even handle * restarting logic, but will not actually spawn any processes and will not * open a session with an existing process. Instead, a fake Session object * is returned which points to a Process object that isn't stored anywhere * in the Pool structures and isn't mapped to any real OS process. It does * however point to the real Group structure. Useful for unit tests. * False by default. */ bool noop; /*-----------------*/ /*-----------------*/ /*********** Spawn options automatically set by Pool *********** * These options are passed to the Spawner. The Pool::get() caller may not * see these values. */ /** The API key of the pool group that the spawned process is to belong to. */ StaticString apiKey; /** * A UUID that's generated on Group initialization, and changes every time * the Group receives a restart command. Allows Union Station to track app * restarts. */ StaticString groupUuid; /*********************************/ /** * Creates a new Options object with the default values filled in. * One must still set appRoot manually, after having used this constructor. */ Options() : logLevel(DEFAULT_LOG_LEVEL), startTimeout(90 * 1000), environment(DEFAULT_APP_ENV, sizeof(DEFAULT_APP_ENV) - 1), baseURI("/", 1), spawnMethod(DEFAULT_SPAWN_METHOD, sizeof(DEFAULT_SPAWN_METHOD) - 1), defaultUser(PASSENGER_DEFAULT_USER, sizeof(PASSENGER_DEFAULT_USER) - 1), lveMinUid(DEFAULT_LVE_MIN_UID), integrationMode(DEFAULT_INTEGRATION_MODE, sizeof(DEFAULT_INTEGRATION_MODE) - 1), ruby(DEFAULT_RUBY, sizeof(DEFAULT_RUBY) - 1), python(DEFAULT_PYTHON, sizeof(DEFAULT_PYTHON) - 1), nodejs(DEFAULT_NODEJS, sizeof(DEFAULT_NODEJS) - 1), fileDescriptorUlimit(0), forceMaxConcurrentRequestsPerProcess(-1), debugger(false), loadShellEnvvars(true), userSwitching(true), analytics(false), raiseInternalError(false), minProcesses(1), maxProcesses(0), maxPreloaderIdleTime(-1), maxOutOfBandWorkInstances(1), maxRequestQueueSize(100), abortWebsocketsOnProcessShutdown(true), stickySessionId(0), statThrottleRate(DEFAULT_STAT_THROTTLE_RATE), maxRequests(0), currentTime(0), noop(false) /*********************************/ { /*********************************/ } Options copy() const { return *this; } Options copyAndPersist() const { Options cpy(*this); cpy.persist(*this); return cpy; } /** * Assign other's string fields' values into this Option * object, and store the data in this Option object's internal storage * area. */ Options &persist(const Options &other) { vector strings = getStringFields(*this); const vector otherStrings = getStringFields(other); unsigned int i; size_t otherLen = 0; char *end; assert(strings.size() == otherStrings.size()); // Calculate the desired length of the internal storage area. // All strings are NULL-terminated. for (i = 0; i < otherStrings.size(); i++) { otherLen += otherStrings[i]->size() + 1; } shared_array data(new char[otherLen]); end = data.get(); // Copy string fields into the internal storage area. for (i = 0; i < otherStrings.size(); i++) { const char *pos = end; StaticString *str = strings[i]; const StaticString *otherStr = otherStrings[i]; // Copy over the string data. memcpy(end, otherStr->c_str(), otherStr->size()); end += otherStr->size(); *end = '\0'; end++; // Point current object's field to the data in the // internal storage area. *str = StaticString(pos, end - pos - 1); } storage = data; // Fix up HashedStaticStrings' hashes. appRoot.setHash(other.appRoot.hash()); appGroupName.setHash(other.appGroupName.hash()); return *this; } Options &clearPerRequestFields() { hostName = StaticString(); uri = StaticString(); stickySessionId = 0; currentTime = 0; noop = false; return detachFromUnionStationTransaction(); } Options &detachFromUnionStationTransaction() { transaction.reset(); return *this; } enum FieldSet { SPAWN_OPTIONS = 1 << 0, PER_GROUP_POOL_OPTIONS = 1 << 1, ALL_OPTIONS = ~0 }; /** * Append information in this Options object to the given string vector, except * for environmentVariables. You can customize what information you want through * the `elements` argument. */ void toVector(vector &vec, const ResourceLocator &resourceLocator, int fields = ALL_OPTIONS) const { if (fields & SPAWN_OPTIONS) { appendKeyValue (vec, "app_root", appRoot); appendKeyValue (vec, "app_group_name", getAppGroupName()); appendKeyValue (vec, "app_type", appType); appendKeyValue (vec, "start_command", getStartCommand(resourceLocator)); appendKeyValue (vec, "startup_file", absolutizePath(getStartupFile(), absolutizePath(appRoot))); appendKeyValue (vec, "process_title", getProcessTitle()); appendKeyValue2(vec, "log_level", logLevel); appendKeyValue3(vec, "start_timeout", startTimeout); appendKeyValue (vec, "environment", environment); appendKeyValue (vec, "base_uri", baseURI); appendKeyValue (vec, "spawn_method", spawnMethod); appendKeyValue (vec, "user", user); appendKeyValue (vec, "group", group); appendKeyValue (vec, "default_user", defaultUser); appendKeyValue (vec, "default_group", defaultGroup); appendKeyValue (vec, "restart_dir", restartDir); appendKeyValue (vec, "preexec_chroot", preexecChroot); appendKeyValue (vec, "postexec_chroot", postexecChroot); appendKeyValue (vec, "integration_mode", integrationMode); appendKeyValue (vec, "ruby", ruby); appendKeyValue (vec, "python", python); appendKeyValue (vec, "nodejs", nodejs); appendKeyValue (vec, "meteor_app_settings", meteorAppSettings); appendKeyValue (vec, "ust_router_address", ustRouterAddress); appendKeyValue (vec, "ust_router_username", ustRouterUsername); appendKeyValue (vec, "ust_router_password", ustRouterPassword); appendKeyValue4(vec, "debugger", debugger); appendKeyValue4(vec, "analytics", analytics); appendKeyValue (vec, "api_key", apiKey); /*********************************/ } if (fields & PER_GROUP_POOL_OPTIONS) { appendKeyValue3(vec, "min_processes", minProcesses); appendKeyValue3(vec, "max_processes", maxProcesses); appendKeyValue2(vec, "max_preloader_idle_time", maxPreloaderIdleTime); appendKeyValue3(vec, "max_out_of_band_work_instances", maxOutOfBandWorkInstances); } if ((fields & SPAWN_OPTIONS) || (fields & PER_GROUP_POOL_OPTIONS)) { appendKeyValue (vec, "union_station_key", unionStationKey); } /*********************************/ } template void toXml(Stream &stream, const ResourceLocator &resourceLocator, int fields = ALL_OPTIONS) const { vector args; unsigned int i; toVector(args, resourceLocator, fields); for (i = 0; i < args.size(); i += 2) { stream << "<" << args[i] << ">"; stream << escapeForXml(args[i + 1]); stream << ""; } } /** * Returns the app group name. If there is no explicitly set app group name * then the app root is considered to be the app group name. */ const HashedStaticString &getAppGroupName() const { if (appGroupName.empty()) { return appRoot; } else { return appGroupName; } } string getStartCommand(const ResourceLocator &resourceLocator) const { if (appType == P_STATIC_STRING("rack")) { return ruby + "\t" + resourceLocator.getHelperScriptsDir() + "/rack-loader.rb"; } else if (appType == P_STATIC_STRING("wsgi")) { return python + "\t" + resourceLocator.getHelperScriptsDir() + "/wsgi-loader.py"; } else if (appType == P_STATIC_STRING("node")) { return nodejs + "\t" + resourceLocator.getHelperScriptsDir() + "/node-loader.js"; } else if (appType == P_STATIC_STRING("meteor")) { return ruby + "\t" + resourceLocator.getHelperScriptsDir() + "/meteor-loader.rb"; } else { return startCommand; } } StaticString getStartupFile() const { if (startupFile.empty()) { const char *result = getAppTypeStartupFile(getAppType(appType)); if (result == NULL) { return P_STATIC_STRING(""); } else { return result; } } else { return startupFile; } } StaticString getProcessTitle() const { const char *result = getAppTypeProcessTitle(getAppType(appType)); if (result == NULL) { return processTitle; } else { return result; } } unsigned long getMaxPreloaderIdleTime() const { if (maxPreloaderIdleTime == -1) { return DEFAULT_MAX_PRELOADER_IDLE_TIME; } else { return maxPreloaderIdleTime; } } }; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_OPTIONS_H_ */