commit a19c24edc1057bd411821f9e3e7d1d309d38b1bb Author: Ismaël Bouya Date: Sun Feb 3 20:58:18 2019 +0100 Add ldap connection diff --git a/.htaccess b/.htaccess index 4c00427..5acd708 100644 --- a/.htaccess +++ b/.htaccess @@ -6,10 +6,23 @@ RewriteEngine On # Prevent accessing subdirectories not managed by SCM RewriteRule ^(.git|doxygen|vendor) - [F] +RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ +RewriteRule ^(.*) - [E=BASE:%1] + +RewriteCond %{ENV:REDIRECT_BASE} (.+) +RewriteRule .* - [E=BASE:%1] + # Forward the "Authorization" HTTP header RewriteCond %{HTTP:Authorization} ^(.*) RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^((?!api/)[^/]*)/?(.*)$ $2?%{QUERY_STRING} [E=USERSPACE:$1] + +RewriteCond %{ENV:REDIRECT_USERSPACE} (.+) +RewriteRule .* - [E=USERSPACE:%1] + # REST API RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 911873a..f21a1ef 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php @@ -191,6 +191,9 @@ public static function checkResourcePermissions($conf) $conf->get('resource.page_cache'), $conf->get('resource.raintpl_tmp'), ) as $path) { + if (! is_dir($path)) { + mkdir($path, 0755, true); + } if (! is_readable(realpath($path))) { $errors[] = '"'.$path.'" '. t('directory is not readable'); } diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index 32aaea4..99efc15 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -21,6 +21,11 @@ class ConfigManager public static $DEFAULT_PLUGINS = array('qrcode'); + /** + * @var string User space. + */ + protected $userSpace; + /** * @var string Config folder. */ @@ -41,12 +46,36 @@ class ConfigManager * * @param string $configFile Configuration file path without extension. */ - public function __construct($configFile = 'data/config') + public function __construct($configFile = null, $userSpace = null) { - $this->configFile = $configFile; + $this->userSpace = $this->findLDAPUser($userSpace); + if ($configFile !== null) { + $this->configFile = $configFile; + } else { + $this->configFile = ($this->userSpace === null) ? 'data/config' : 'data/' . $this->userSpace . '/config'; + } $this->initialize(); } + public function findLDAPUser($login, $password = null) { + $connect = ldap_connect(getenv('SHAARLI_LDAP_HOST')); + ldap_set_option($connect, LDAP_OPT_PROTOCOL_VERSION, 3); + if (!$connect || !ldap_bind($connect, getenv('SHAARLI_LDAP_DN'), getenv('SHAARLI_LDAP_PASSWORD'))) { + return false; + } + + $search_query = str_replace('%login%', ldap_escape($login), getenv('SHAARLI_LDAP_FILTER')); + + $search = ldap_search($connect, getenv('SHAARLI_LDAP_BASE'), $search_query); + $info = ldap_get_entries($connect, $search); + + if (ldap_count_entries($connect, $search) == 1 && (is_null($password) || ldap_bind($connect, $info[0]["dn"], $password))) { + return $login; + } else { + return null; + } + } + /** * Reset the ConfigManager instance. */ @@ -269,6 +298,16 @@ public function getConfigFileExt() return $this->configFile . $this->configIO->getExtension(); } + /** + * Get the current userspace. + * + * @return mixed User space. + */ + public function getUserSpace() + { + return $this->userSpace; + } + /** * Recursive function which find asked setting in the loaded config. * @@ -342,19 +381,31 @@ protected static function removeConfig($settings, &$conf) */ protected function setDefaultValues() { - $this->setEmpty('resource.data_dir', 'data'); - $this->setEmpty('resource.config', 'data/config.php'); - $this->setEmpty('resource.datastore', 'data/datastore.php'); - $this->setEmpty('resource.ban_file', 'data/ipbans.php'); - $this->setEmpty('resource.updates', 'data/updates.txt'); - $this->setEmpty('resource.log', 'data/log.txt'); - $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt'); - $this->setEmpty('resource.history', 'data/history.php'); + if ($this->userSpace === null) { + $data = 'data'; + $tmp = 'tmp/'; + $cache = 'cache'; + $pagecache = 'pagecache'; + } else { + $data = 'data/' . ($this->userSpace); + $tmp = 'tmp/' . ($this->userSpace) . '/'; + $cache = 'cache/' . ($this->userSpace); + $pagecache = 'pagecache/' . ($this->userSpace); + } + + $this->setEmpty('resource.data_dir', $data); + $this->setEmpty('resource.config', $data . '/config.php'); + $this->setEmpty('resource.datastore', $data . '/datastore.php'); + $this->setEmpty('resource.ban_file', $data . '/ipbans.php'); + $this->setEmpty('resource.updates', $data . '/updates.txt'); + $this->setEmpty('resource.log', $data . '/log.txt'); + $this->setEmpty('resource.update_check', $data . '/lastupdatecheck.txt'); + $this->setEmpty('resource.history', $data . '/history.php'); $this->setEmpty('resource.raintpl_tpl', 'tpl/'); $this->setEmpty('resource.theme', 'default'); - $this->setEmpty('resource.raintpl_tmp', 'tmp/'); - $this->setEmpty('resource.thumbnails_cache', 'cache'); - $this->setEmpty('resource.page_cache', 'pagecache'); + $this->setEmpty('resource.raintpl_tmp', $tmp); + $this->setEmpty('resource.thumbnails_cache', $cache); + $this->setEmpty('resource.page_cache', $pagecache); $this->setEmpty('security.ban_after', 4); $this->setEmpty('security.ban_duration', 1800); diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index d6784d6..bdfaca7 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -32,6 +32,9 @@ class LoginManager /** @var string User sign-in token depending on remote IP and credentials */ protected $staySignedInToken = ''; + protected $lastErrorReason = ''; + protected $lastErrorIsBanishable = false; + /** * Constructor * @@ -83,7 +86,7 @@ public function getStaySignedInToken() */ public function checkLoginState($cookie, $clientIpId) { - if (! $this->configManager->exists('credentials.login')) { + if (! $this->configManager->exists('credentials.login') || (isset($_SESSION['username']) && $_SESSION['username'] && $this->configManager->get('credentials.login') !== $_SESSION['username'])) { // Shaarli is not configured yet $this->isLoggedIn = false; return; @@ -133,20 +136,40 @@ public function isLoggedIn() */ public function checkCredentials($remoteIp, $clientIpId, $login, $password) { - $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); + $this->lastErrorIsBanishable = false; + + if ($this->configManager->getUserSpace() !== null && $this->configManager->getUserSpace() !== $login) { + logm($this->configManager->get('resource.log'), + $remoteIp, + 'Trying to login to wrong user space'); + $this->lastErrorReason = 'You’re trying to access the wrong account.'; + return false; + } - if ($login != $this->configManager->get('credentials.login') - || $hash != $this->configManager->get('credentials.hash') - ) { + logm($this->configManager->get('resource.log'), + $remoteIp, + 'Trying LDAP connection'); + $result = $this->configManager->findLDAPUser($login, $password); + if ($result === false) { logm( $this->configManager->get('resource.log'), $remoteIp, - 'Login failed for user ' . $login + 'Impossible to connect to LDAP' ); + $this->lastErrorReason = 'Server error.'; + return false; + } else if (is_null($result)) { + logm( + $this->configManager->get('resource.log'), + $remoteIp, + 'Login failed for user ' . $login + ); + $this->lastErrorIsBanishable = true; + $this->lastErrorReason = 'Wrong login/password.'; return false; } - $this->sessionManager->storeLoginInfo($clientIpId); + $this->sessionManager->storeLoginInfo($clientIpId, $login); logm( $this->configManager->get('resource.log'), $remoteIp, @@ -187,6 +210,10 @@ protected function writeBanFile() */ public function handleFailedLogin($server) { + if (!$this->lastErrorIsBanishable) { + return $this->lastErrorReason ?: 'Error during login.'; + }; + $ip = $server['REMOTE_ADDR']; $trusted = $this->configManager->get('security.trusted_proxies', []); @@ -215,6 +242,7 @@ public function handleFailedLogin($server) ); } $this->writeBanFile(); + return $this->lastErrorReason ?: 'Error during login.'; } /** diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index b8b8ab8..5eb4aac 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -111,10 +111,10 @@ public static function checkId($sessionId) * * @param string $clientIpId Client IP address identifier */ - public function storeLoginInfo($clientIpId) + public function storeLoginInfo($clientIpId, $login = null) { $this->session['ip'] = $clientIpId; - $this->session['username'] = $this->conf->get('credentials.login'); + $this->session['username'] = $login ?: $this->conf->get('credentials.login'); $this->extendTimeValidityBy(self::$SHORT_TIMEOUT); } diff --git a/index.php b/index.php index 4b86a3e..58ae2dd 100644 --- a/index.php +++ b/index.php @@ -121,7 +121,32 @@ $_COOKIE['shaarli'] = session_id(); } -$conf = new ConfigManager(); +$folderBase = getenv("BASE"); + +if (getenv("USERSPACE")) { + if (isset($_GET["do"]) && $_GET["do"] == "login") { + header("Location: $folderBase/?do=login"); + exit; + } + $userspace = preg_replace("/[^-_A-Za-z0-9]/", '', getenv("USERSPACE")); +} else if (isset($_SESSION["username"]) && $_SESSION["username"]) { + header("Location: " . $folderBase . "/" . $_SESSION["username"] . "?"); + exit; +} else if (!isset($_GET["do"]) || $_GET["do"] != "login") { + header("Location: $folderBase/?do=login"); + exit; +} + +if (!isset($userspace) && isset($_POST["login"])) { + $userspace = preg_replace("/[^-_A-Za-z0-9]/", '', $_POST["login"]); + error_log("debugImmae: setting userspace from POST: " . $userspace); +} + +if (isset($userspace)) { + $conf = new ConfigManager(null, $userspace); +} else { + $conf = new ConfigManager(); +} $sessionManager = new SessionManager($_SESSION, $conf); $loginManager = new LoginManager($GLOBALS, $conf, $sessionManager); $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); @@ -175,7 +200,7 @@ } // Display the installation form if no existing config is found - install($conf, $sessionManager, $loginManager); + install($conf, $sessionManager, $loginManager, $userspace); } $loginManager->checkLoginState($_COOKIE, $clientIpId); @@ -205,6 +230,7 @@ function isLoggedIn() && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password']) ) { $loginManager->handleSuccessfulLogin($_SERVER); + $userspace = $_POST['login']; $cookiedir = ''; if (dirname($_SERVER['SCRIPT_NAME']) != '/') { @@ -241,25 +267,25 @@ function isLoggedIn() $uri .= '&'.$param.'='.urlencode($_GET[$param]); } } - header('Location: '. $uri); + header('Location: '. $userspace . $uri); exit; } if (isset($_GET['edit_link'])) { - header('Location: ?edit_link='. escape($_GET['edit_link'])); + header('Location: ' . $userspace . '?edit_link='. escape($_GET['edit_link'])); exit; } if (isset($_POST['returnurl'])) { // Prevent loops over login screen. if (strpos($_POST['returnurl'], 'do=login') === false) { - header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST'])); + header('Location: ' . generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST'])); exit; } } - header('Location: ?'); exit; + header('Location: '. $userspace . '?'); exit; } else { - $loginManager->handleFailedLogin($_SERVER); + $errorReason = $loginManager->handleFailedLogin($_SERVER); $redir = '&username='. urlencode($_POST['login']); if (isset($_GET['post'])) { $redir .= '&post=' . urlencode($_GET['post']); @@ -270,7 +296,7 @@ function isLoggedIn() } } // Redirect to login screen. - echo ''; + echo ''; exit; } } @@ -1719,7 +1745,7 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) * @param SessionManager $sessionManager SessionManager instance * @param LoginManager $loginManager LoginManager instance */ -function install($conf, $sessionManager, $loginManager) { +function install($conf, $sessionManager, $loginManager, $userspace) { // On free.fr host, make sure the /sessions directory exists, otherwise login will not work. if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705); @@ -1755,7 +1781,7 @@ function install($conf, $sessionManager, $loginManager) { } - if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) + if (true) { $tz = 'UTC'; if (!empty($_POST['continent']) && !empty($_POST['city']) @@ -1764,15 +1790,15 @@ function install($conf, $sessionManager, $loginManager) { $tz = $_POST['continent'].'/'.$_POST['city']; } $conf->set('general.timezone', $tz); - $login = $_POST['setlogin']; - $conf->set('credentials.login', $login); + $conf->set('credentials.login', $userspace); $salt = sha1(uniqid('', true) .'_'. mt_rand()); $conf->set('credentials.salt', $salt); - $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt)); + $hash = sha1(uniqid('', true) .'_'. mt_rand()); + $conf->set('credentials.hash', $hash); if (!empty($_POST['title'])) { $conf->set('general.title', escape($_POST['title'])); } else { - $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER))); + $conf->set('general.title', ucwords(str_replace("_", " ", $userspace))); } $conf->set('translation.language', escape($_POST['language'])); $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); @@ -1841,7 +1867,12 @@ function install($conf, $sessionManager, $loginManager) { $app = new \Slim\App($container); // REST API routes -$app->group('/api/v1', function() { +if (isset($userspace)) { + $mountpoint = '/' . $userspace . '/api/v1'; +} else { + $mountpoint = '/api/v1'; +} +$app->group($mountpoint, function() { $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo'); $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks'); $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink'); @@ -1860,7 +1891,7 @@ function install($conf, $sessionManager, $loginManager) { $response = $app->run(true); // Hack to make Slim and Shaarli router work together: // If a Slim route isn't found and NOT API call, we call renderPage(). -if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { +if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], $mountpoint) === false) { // We use UTF-8 for proper international characters handling. header('Content-Type: text/html; charset=utf-8'); renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);