0.6.0-1 release
[manu/suphp.git] / src / Application.cpp
diff --git a/src/Application.cpp b/src/Application.cpp
new file mode 100644 (file)
index 0000000..dd2a02e
--- /dev/null
@@ -0,0 +1,486 @@
+/*
+    suPHP - (c)2002-2005 Sebastian Marsching <sebastian@marsching.com>
+
+    This file is part of suPHP.
+
+    suPHP 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 2 of the License, or
+    (at your option) any later version.
+
+    suPHP 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 suPHP; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+*/
+
+#include <iostream>
+
+#include "config.h"
+
+#include "CommandLine.hpp"
+#include "Environment.hpp"
+#include "Exception.hpp"
+#include "File.hpp"
+#include "Configuration.hpp"
+#include "API.hpp"
+#include "API_Helper.hpp"
+#include "Logger.hpp"
+#include "UserInfo.hpp"
+#include "GroupInfo.hpp"
+#include "Util.hpp"
+
+#include "Application.hpp"
+
+using namespace suPHP;
+
+
+suPHP::Application::Application() {
+    /* do nothing */
+}
+
+
+int suPHP::Application::run(CommandLine& cmdline, Environment& env) {
+    Configuration config;
+    API& api = API_Helper::getSystemAPI();
+    Logger& logger = api.getSystemLogger();
+    
+#ifdef OPT_CONFIGFILE
+    File cfgFile = File(OPT_CONFIGFILE);
+#else
+    File cfgFile = File("/etc/suphp.conf");
+#endif
+
+    std::string interpreter;
+    TargetMode targetMode;
+    Environment newEnv;
+
+    // Begin try block - soft exception cannot really be handled before
+    // initialization
+    try {
+       std::string scriptFilename;
+       
+       // If caller is super-user, print info message and exit
+       if (api.getRealProcessUser().isSuperUser()) {
+           this->printAboutMessage();
+           return 0;
+       }
+       config.readFromFile(cfgFile);
+       
+       // Check permissions (real uid, effective uid)
+       this->checkProcessPermissions(config);
+       
+       // Initialize logger
+       // not done before, because we need super-user privileges for
+       // logging anyway
+       logger.init(config);
+
+       try {
+           scriptFilename = env.getVar("SCRIPT_FILENAME");
+       } catch (KeyNotFoundException& e) {
+           logger.logError("Environment variable SCRIPT_FILENAME not set");
+           this->printAboutMessage();
+           return 1;
+       }
+
+       this->checkScriptFile(scriptFilename, config, env);
+
+       this->changeProcessPermissions(scriptFilename, config, env);
+
+       interpreter = this->getInterpreter(env, config);
+       targetMode = this->getTargetMode(interpreter);
+       
+       // Prepare environment for new process
+       newEnv = this->prepareEnvironment(env, config, targetMode);
+       
+       // Log attempt to execute script
+       logger.logInfo("Executing \"" + scriptFilename + "\" as UID "
+                      + Util::intToStr(api.getEffectiveProcessUser().getUid())
+                      + ", GID " 
+                      + Util::intToStr(
+                          api.getEffectiveProcessGroup().getGid()));
+
+       this->executeScript(scriptFilename, interpreter, targetMode, newEnv, 
+                           config);
+       
+       // Function should never return
+       // So, if we get here, return with error code
+       return 1;
+    } catch (SoftException& e) {
+       if (!config.getErrorsToBrowser()) {
+           std::cerr << e;
+           return 2;
+       }
+       std::cout << "Content-Type: text/html\n"
+                 << "Status: 500\n"
+                 << "\n"
+                 << "<html>\n"
+                 << " <head>\n"
+                 << "  <title>500 Internal Server Error</title>\n"
+                 << " </head>\n"
+                 << " <body>\n"
+                 << "  <h1>Internal Server Error</h1>\n"
+                 << "  <p>" << e.getMessage() << "</p>\n"
+                 << "  <hr/>"
+                 << "  <address>suPHP " << PACKAGE_VERSION << "</address>\n"
+                 << " </body>\n"
+                 << "</html>\n";
+    }
+}
+
+
+void suPHP::Application::printAboutMessage() {
+    std::cerr << "suPHP version " << PACKAGE_VERSION << "\n";
+    std::cerr << "(c) 2002-2005 Sebastian Marsching\n";
+    std::cerr << std::endl;
+    std::cerr << "suPHP has to be called by mod_suphp to work." << std::endl;
+}
+
+
+void suPHP::Application::checkProcessPermissions(Configuration& config) 
+    throw (SecurityException, LookupException) {
+    API& api = API_Helper::getSystemAPI();
+    if (api.getRealProcessUser() !=
+       api.getUserInfo(config.getWebserverUser())) {
+       throw SecurityException("Calling user is not webserver user!",
+                               __FILE__, __LINE__);
+    }
+    
+    if (!api.getEffectiveProcessUser().isSuperUser()) {
+       throw SecurityException(
+           "Do not have root privileges. Executable not set-uid root?",
+           __FILE__, __LINE__);
+    }
+}
+
+
+void suPHP::Application::checkScriptFile(
+    const std::string& scriptFilename, 
+    const Configuration& config,
+    const Environment& environment) const
+    throw (SystemException, SoftException) {
+    Logger& logger = API_Helper::getSystemAPI().getSystemLogger();
+    File scriptFile = File(scriptFilename);
+
+    // Check wheter file exists
+    if (!scriptFile.exists()) {
+       std::string error = "File " + scriptFile.getPath() + " does not exist";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+    
+    // Get full path to script file
+
+    File realScriptFile = File(scriptFile.getRealPath());
+    File directory = realScriptFile.getParentDirectory();
+    
+    // Check wheter script is in docroot
+    if (realScriptFile.getPath().find(config.getDocroot()) != 0) {
+       std::string error = "Script \"" + scriptFile.getPath() 
+           + "\" resolving to \"" + realScriptFile.getPath() 
+           + "\" not within configured docroot";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+
+    // If enabled, check whether script is in the vhost's docroot
+    if (!environment.hasVar("DOCUMENT_ROOT"))
+       throw SoftException("Environment variable DOCUMENT_ROOT not set",
+                               __FILE__, __LINE__);
+    if (config.getCheckVHostDocroot()
+       && realScriptFile.getPath().find(environment.getVar("DOCUMENT_ROOT")) 
+       != 0) {
+       
+       std::string error = "File \"" + realScriptFile.getPath()
+           + "\" is not in document root of Vhost \""
+           + environment.getVar("DOCUMENT_ROOT") + "\"";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+
+    // Check script and directory permissions
+    if (!realScriptFile.hasUserReadBit()) {
+       std::string error = "File \"" + realScriptFile.getPath()
+           + "\" not readable";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+       
+    }
+
+    if (!config.getAllowFileGroupWriteable()
+       && realScriptFile.hasGroupWriteBit()) {
+       std::string error = "File \"" + realScriptFile.getPath()
+           + "\" is writeable by group";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+    
+    if (!config.getAllowDirectoryGroupWriteable() 
+       && directory.hasGroupWriteBit()) {
+       std::string error = "Directory \"" + directory.getPath()
+           + "\" is writeable by group";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+
+    if (!config.getAllowFileOthersWriteable()
+       && realScriptFile.hasOthersWriteBit()) {
+       std::string error = "File \"" + realScriptFile.getPath()
+           + "\" is writeable by others";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+    
+    if (!config.getAllowDirectoryOthersWriteable()
+       && directory.hasOthersWriteBit()) {
+       std::string error = "Directory \"" + directory.getPath()
+           + "\" is writeable by others";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+
+    // Check UID/GID of symlink is matching target
+    if (scriptFile.getUser() != realScriptFile.getUser()
+       || scriptFile.getGroup() != realScriptFile.getGroup()) {
+       std::string error = "UID or GID of symlink \"" + scriptFile.getPath() 
+           + "\" is not matching its target";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+}
+
+
+void suPHP::Application::changeProcessPermissions(
+    const std::string& scriptFilename, 
+    const Configuration& config,
+    const Environment& environment) const
+    throw (SystemException, SoftException, SecurityException) {
+    UserInfo targetUser;
+    GroupInfo targetGroup;
+
+    File scriptFile = File(File(scriptFilename).getRealPath());
+    API& api = API_Helper::getSystemAPI();
+    Logger& logger = api.getSystemLogger();
+
+    // Make sure that exactly one mode is set
+
+#if !defined(OPT_USERGROUP_OWNER) && !defined(OPT_USERGROUP_FORCE) && !defined(OPT_USERGROUP_PARANOID)
+#error "No uid/gid change model specified"
+#endif
+#if (defined(OPT_USERGROUP_OWNER) && defined(OPT_USERGROUP_FORCE)) || (defined(OPT_USERGROUP_FORCE) && defined(OPT_USERGROUP_PARANOID)) || (defined(OPT_USERGROUP_OWNER) && defined(OPT_USERGROUP_PARANOID))
+#error "More than one uid/gid change model specified"
+#endif
+
+    // Common code (for all security modes)
+
+    // Check UID/GID of script
+    if (scriptFile.getUser().getUid() < config.getMinUid()) {
+       std::string error = "UID of script \"" + scriptFilename
+           + "\" is smaller than min_uid";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+    if (scriptFile.getGroup().getGid() < config.getMinGid()) {
+       std::string error = "GID of script \"" + scriptFilename
+           + "\" is smaller than min_gid";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+    
+    // Paranoid and force mode
+
+#if (defined(OPT_USERGROUP_PARANOID) || defined(OPT_USERGROUP_FORCE))
+    std::string targetUsername, targetGroupname;
+    try {
+       targetUsername = environment.getVar("SUPHP_USER");
+       targetGroupname = environment.getVar("SUPHP_GROUP");
+    } catch (KeyNotFoundException& e) {
+       throw SecurityException(
+           "Environment variable SUPHP_USER or SUPHP_GROUP not set", 
+           __FILE__, __LINE__);
+    }
+    
+    if (targetUsername[0] == '#' && targetUsername.find_first_not_of(
+           "0123456789", 1) == std::string::npos) {
+       targetUser = api.getUserInfo(Util::strToInt(targetUsername.substr(1)));
+    } else {
+       targetUser = api.getUserInfo(targetUsername);
+    }
+
+    if (targetGroupname[0] == '#' && targetGroupname.find_first_not_of(
+           "0123456789", 1) == std::string::npos) {
+       targetGroup = api.getGroupInfo(
+           Util::strToInt(targetGroupname.substr(1)));
+    } else {
+       targetGroup = api.getGroupInfo(targetGroupname);
+    }
+#endif // OPT_USERGROUP_PARANOID || OPT_USERGROUP_FORCE
+
+    // Owner mode only
+
+#ifdef OPT_USERGROUP_OWNER
+    targetUser = scriptFile.getUser();
+    targetGroup = scriptFile.getGroup();
+#endif // OPT_USERGROUP_OWNER
+    
+    // Paranoid mode only
+
+#ifdef OPT_USERGROUP_PARANOID
+    if (targetUser != scriptFile.getUser()) {
+       std::string error ="Mismatch between target UID ("
+           + Util::intToStr(targetUser.getUid()) + ") and UID (" 
+           + Util::intToStr(scriptFile.getUser().getUid()) + ") of file \"" 
+           + scriptFile.getPath() + "\"";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+
+    if (targetGroup != scriptFile.getGroup()) {
+       std::string error ="Mismatch between target GID ("
+           + Util::intToStr(targetGroup.getGid()) + ") and GID (" 
+           + Util::intToStr(scriptFile.getGroup().getGid()) + ") of file \"" 
+           + scriptFile.getPath() + "\"";
+       logger.logWarning(error);
+       throw SoftException(error, __FILE__, __LINE__);
+    }
+#endif // OPT_USERGROUP_PARANOID    
+
+    // Common code used for all modes
+
+    // Set new group first, because we still need super-user privileges
+    // for this
+    api.setProcessGroup(targetGroup);
+    
+    // Then set new user
+    api.setProcessUser(targetUser);
+
+    api.setUmask(config.getUmask());
+}
+
+
+Environment suPHP::Application::prepareEnvironment(
+    const Environment& sourceEnv, const Configuration& config, TargetMode mode)
+    throw (KeyNotFoundException) {
+    // Create environment for new process from old environment
+    Environment env = sourceEnv;
+    
+    // Delete unwanted environment variables
+    if (env.hasVar("LD_PRELOAD"))
+       env.deleteVar("LD_PRELOAD");
+    if (env.hasVar("LD_LIBRARY_PATH"))
+       env.deleteVar("LD_LIBRARY_PATH");
+    if (env.hasVar("SUPHP_USER"))
+       env.deleteVar("SUPHP_USER");
+    if (env.hasVar("SUPHP_GROUP"))
+       env.deleteVar("SUPHP_GROUP");
+    if (env.hasVar("SUPHP_HANDLER"))
+       env.deleteVar("SUPHP_HANDLER");
+    if (env.hasVar("SUPHP_AUTH_USER"))
+       env.deleteVar("SUPHP_AUTH_USER");
+    if (env.hasVar("SUPHP_AUTH_PW"))
+       env.deleteVar("SUPHP_AUTH_PW");
+    if (env.hasVar("SUPHP_PHP_CONFIG"))
+       env.deleteVar("SUPHP_PHP_CONFIG");
+    
+    // Reset PATH
+    env.putVar("PATH", config.getEnvPath());
+
+    // If we are in PHP mode, set PHP specific variables
+    if (mode == TARGETMODE_PHP) {
+       if (sourceEnv.hasVar("SUPHP_PHP_CONFIG"))
+           env.putVar("PHPRC", sourceEnv.getVar("SUPHP_PHP_CONFIG"));
+       if (sourceEnv.hasVar("SUPHP_AUTH_USER")
+           && sourceEnv.hasVar("SUPHP_AUTH_PW")) {
+           env.putVar("PHP_AUTH_USER", sourceEnv.getVar("SUPHP_AUTH_USER"));
+           env.putVar("PHP_AUTH_PW", sourceEnv.getVar("SUPHP_AUTH_PW"));
+       }
+
+       // PHP may need this, when compiled with security features
+       if (!env.hasVar("REDIRECT_STATUS")) {
+           env.putVar("REDIRECT_STATUS", "200");
+       }
+    }
+    
+    return env;
+}
+
+
+std::string suPHP::Application::getInterpreter(
+    const Environment& env, const Configuration& config)
+    throw (SecurityException) {
+    if (!env.hasVar("SUPHP_HANDLER"))
+       throw SecurityException("Environment variable SUPHP_HANDLER not set",
+                               __FILE__, __LINE__);
+    std::string handler = env.getVar("SUPHP_HANDLER");
+    
+    std::string interpreter = "";
+    try {
+       interpreter = config.getInterpreter(handler);
+    } catch (KeyNotFoundException& e) {
+       throw SecurityException ("Handler not found in configuration", e,
+                                __FILE__, __LINE__);
+    }
+    
+    return interpreter;
+}
+
+
+TargetMode suPHP::Application::getTargetMode(const std::string& interpreter)
+    throw (SecurityException) {
+    if (interpreter.substr(0, 4) == "php:")
+       return TARGETMODE_PHP;
+    else if (interpreter == "execute:!self")
+       return TARGETMODE_SELFEXECUTE;
+    else
+       throw SecurityException("Unknown Interpreter: " + interpreter,
+                               __FILE__, __LINE__);
+}
+
+
+void suPHP::Application::executeScript(const std::string& scriptFilename,
+                                      const std::string& interpreter,
+                                      TargetMode mode,
+                                      const Environment& env,
+                                      const Configuration& config) const
+    throw (SoftException) {
+    try {
+       // Change working directory to script path
+       API_Helper::getSystemAPI().setCwd(
+           File(scriptFilename).getParentDirectory().getPath());
+       if (mode == TARGETMODE_PHP) {
+           std::string interpreterPath = interpreter.substr(4);
+           CommandLine cline;
+           cline.putArgument(interpreterPath);
+           API_Helper::getSystemAPI().execute(interpreterPath, cline, env);
+       } else if (mode == TARGETMODE_SELFEXECUTE) {
+           CommandLine cline;
+           cline.putArgument(scriptFilename);
+           API_Helper::getSystemAPI().execute(scriptFilename, cline, env);
+       }
+    } catch (SystemException& e) {
+       throw SoftException("Could not execute script \"" + scriptFilename
+                               + "\"", e, __FILE__, __LINE__);
+    }
+}
+
+
+int main(int argc, char **argv) {
+    try {
+       API& api = API_Helper::getSystemAPI();
+       CommandLine cmdline;
+       Environment env;
+       Application app;
+       for (int i=0; i<argc; i++) {
+           cmdline.putArgument(argv[i]);
+       }
+       env = api.getProcessEnvironment();
+       return app.run(cmdline, env);
+    } catch (Exception& e) {
+       std::cerr << e;
+       return 1;
+    }
+}