]> git.immae.eu Git - perso/Immae/Config/Nix.git/blame - overlays/shaarli/shaarli_ldap.patch
Fix shaarli long sessions
[perso/Immae/Config/Nix.git] / overlays / shaarli / shaarli_ldap.patch
CommitLineData
5752291a 1commit a19c24edc1057bd411821f9e3e7d1d309d38b1bb
95b20e17
IB
2Author: Ismaël Bouya <ismael.bouya@normalesup.org>
3Date: Sun Feb 3 20:58:18 2019 +0100
4
5 Add ldap connection
6
7diff --git a/.htaccess b/.htaccess
8index 4c00427..5acd708 100644
9--- a/.htaccess
10+++ b/.htaccess
11@@ -6,10 +6,23 @@ RewriteEngine On
12 # Prevent accessing subdirectories not managed by SCM
13 RewriteRule ^(.git|doxygen|vendor) - [F]
14
15+RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
16+RewriteRule ^(.*) - [E=BASE:%1]
17+
18+RewriteCond %{ENV:REDIRECT_BASE} (.+)
19+RewriteRule .* - [E=BASE:%1]
20+
21 # Forward the "Authorization" HTTP header
22 RewriteCond %{HTTP:Authorization} ^(.*)
23 RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
24
25+RewriteCond %{REQUEST_FILENAME} !-f
26+RewriteCond %{REQUEST_FILENAME} !-d
27+RewriteRule ^((?!api/)[^/]*)/?(.*)$ $2?%{QUERY_STRING} [E=USERSPACE:$1]
28+
29+RewriteCond %{ENV:REDIRECT_USERSPACE} (.+)
30+RewriteRule .* - [E=USERSPACE:%1]
31+
32 # REST API
33 RewriteCond %{REQUEST_FILENAME} !-f
34 RewriteCond %{REQUEST_FILENAME} !-d
35diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php
36index 911873a..f21a1ef 100644
37--- a/application/ApplicationUtils.php
38+++ b/application/ApplicationUtils.php
39@@ -191,6 +191,9 @@ public static function checkResourcePermissions($conf)
40 $conf->get('resource.page_cache'),
41 $conf->get('resource.raintpl_tmp'),
42 ) as $path) {
43+ if (! is_dir($path)) {
44+ mkdir($path, 0755, true);
45+ }
46 if (! is_readable(realpath($path))) {
47 $errors[] = '"'.$path.'" '. t('directory is not readable');
48 }
49diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
50index 32aaea4..99efc15 100644
51--- a/application/config/ConfigManager.php
52+++ b/application/config/ConfigManager.php
53@@ -21,6 +21,11 @@ class ConfigManager
54
55 public static $DEFAULT_PLUGINS = array('qrcode');
56
57+ /**
58+ * @var string User space.
59+ */
60+ protected $userSpace;
61+
62 /**
63 * @var string Config folder.
64 */
65@@ -41,12 +46,36 @@ class ConfigManager
66 *
67 * @param string $configFile Configuration file path without extension.
68 */
69- public function __construct($configFile = 'data/config')
70+ public function __construct($configFile = null, $userSpace = null)
71 {
72- $this->configFile = $configFile;
73+ $this->userSpace = $this->findLDAPUser($userSpace);
74+ if ($configFile !== null) {
75+ $this->configFile = $configFile;
76+ } else {
77+ $this->configFile = ($this->userSpace === null) ? 'data/config' : 'data/' . $this->userSpace . '/config';
78+ }
79 $this->initialize();
80 }
81
82+ public function findLDAPUser($login, $password = null) {
83+ $connect = ldap_connect(getenv('SHAARLI_LDAP_HOST'));
84+ ldap_set_option($connect, LDAP_OPT_PROTOCOL_VERSION, 3);
85+ if (!$connect || !ldap_bind($connect, getenv('SHAARLI_LDAP_DN'), getenv('SHAARLI_LDAP_PASSWORD'))) {
86+ return false;
87+ }
88+
89+ $search_query = str_replace('%login%', ldap_escape($login), getenv('SHAARLI_LDAP_FILTER'));
90+
91+ $search = ldap_search($connect, getenv('SHAARLI_LDAP_BASE'), $search_query);
92+ $info = ldap_get_entries($connect, $search);
93+
94+ if (ldap_count_entries($connect, $search) == 1 && (is_null($password) || ldap_bind($connect, $info[0]["dn"], $password))) {
95+ return $login;
96+ } else {
97+ return null;
98+ }
99+ }
100+
101 /**
102 * Reset the ConfigManager instance.
103 */
104@@ -269,6 +298,16 @@ public function getConfigFileExt()
105 return $this->configFile . $this->configIO->getExtension();
106 }
107
108+ /**
109+ * Get the current userspace.
110+ *
111+ * @return mixed User space.
112+ */
113+ public function getUserSpace()
114+ {
115+ return $this->userSpace;
116+ }
117+
118 /**
119 * Recursive function which find asked setting in the loaded config.
120 *
121@@ -342,19 +381,31 @@ protected static function removeConfig($settings, &$conf)
122 */
123 protected function setDefaultValues()
124 {
125- $this->setEmpty('resource.data_dir', 'data');
126- $this->setEmpty('resource.config', 'data/config.php');
127- $this->setEmpty('resource.datastore', 'data/datastore.php');
128- $this->setEmpty('resource.ban_file', 'data/ipbans.php');
129- $this->setEmpty('resource.updates', 'data/updates.txt');
130- $this->setEmpty('resource.log', 'data/log.txt');
131- $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
132- $this->setEmpty('resource.history', 'data/history.php');
133+ if ($this->userSpace === null) {
134+ $data = 'data';
135+ $tmp = 'tmp';
136+ $cache = 'cache';
137+ $pagecache = 'pagecache';
138+ } else {
139+ $data = 'data/' . ($this->userSpace);
140+ $tmp = 'tmp/' . ($this->userSpace);
141+ $cache = 'cache/' . ($this->userSpace);
142+ $pagecache = 'pagecache/' . ($this->userSpace);
143+ }
144+
145+ $this->setEmpty('resource.data_dir', $data);
146+ $this->setEmpty('resource.config', $data . '/config.php');
147+ $this->setEmpty('resource.datastore', $data . '/datastore.php');
148+ $this->setEmpty('resource.ban_file', $data . '/ipbans.php');
149+ $this->setEmpty('resource.updates', $data . '/updates.txt');
150+ $this->setEmpty('resource.log', $data . '/log.txt');
151+ $this->setEmpty('resource.update_check', $data . '/lastupdatecheck.txt');
152+ $this->setEmpty('resource.history', $data . '/history.php');
153 $this->setEmpty('resource.raintpl_tpl', 'tpl/');
154 $this->setEmpty('resource.theme', 'default');
155- $this->setEmpty('resource.raintpl_tmp', 'tmp/');
156- $this->setEmpty('resource.thumbnails_cache', 'cache');
157- $this->setEmpty('resource.page_cache', 'pagecache');
158+ $this->setEmpty('resource.raintpl_tmp', $tmp);
159+ $this->setEmpty('resource.thumbnails_cache', $cache);
160+ $this->setEmpty('resource.page_cache', $pagecache);
161
162 $this->setEmpty('security.ban_after', 4);
163 $this->setEmpty('security.ban_duration', 1800);
164diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php
165index d6784d6..bdfaca7 100644
166--- a/application/security/LoginManager.php
167+++ b/application/security/LoginManager.php
168@@ -32,6 +32,9 @@ class LoginManager
169 /** @var string User sign-in token depending on remote IP and credentials */
170 protected $staySignedInToken = '';
171
172+ protected $lastErrorReason = '';
173+ protected $lastErrorIsBanishable = false;
174+
175 /**
176 * Constructor
177 *
178@@ -83,7 +86,7 @@ public function getStaySignedInToken()
179 */
180 public function checkLoginState($cookie, $clientIpId)
181 {
182- if (! $this->configManager->exists('credentials.login')) {
183+ if (! $this->configManager->exists('credentials.login') || (isset($_SESSION['username']) && $_SESSION['username'] && $this->configManager->get('credentials.login') !== $_SESSION['username'])) {
184 // Shaarli is not configured yet
185 $this->isLoggedIn = false;
186 return;
187@@ -133,20 +136,40 @@ public function isLoggedIn()
188 */
189 public function checkCredentials($remoteIp, $clientIpId, $login, $password)
190 {
191- $hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
192+ $this->lastErrorIsBanishable = false;
193+
194+ if ($this->configManager->getUserSpace() !== null && $this->configManager->getUserSpace() !== $login) {
195+ logm($this->configManager->get('resource.log'),
196+ $remoteIp,
197+ 'Trying to login to wrong user space');
198+ $this->lastErrorReason = 'You’re trying to access the wrong account.';
199+ return false;
200+ }
201
202- if ($login != $this->configManager->get('credentials.login')
203- || $hash != $this->configManager->get('credentials.hash')
204- ) {
205+ logm($this->configManager->get('resource.log'),
206+ $remoteIp,
207+ 'Trying LDAP connection');
208+ $result = $this->configManager->findLDAPUser($login, $password);
209+ if ($result === false) {
210 logm(
211 $this->configManager->get('resource.log'),
212 $remoteIp,
213- 'Login failed for user ' . $login
214+ 'Impossible to connect to LDAP'
215 );
216+ $this->lastErrorReason = 'Server error.';
217+ return false;
218+ } else if (is_null($result)) {
219+ logm(
220+ $this->configManager->get('resource.log'),
221+ $remoteIp,
222+ 'Login failed for user ' . $login
223+ );
224+ $this->lastErrorIsBanishable = true;
225+ $this->lastErrorReason = 'Wrong login/password.';
226 return false;
227 }
228
229- $this->sessionManager->storeLoginInfo($clientIpId);
230+ $this->sessionManager->storeLoginInfo($clientIpId, $login);
231 logm(
232 $this->configManager->get('resource.log'),
233 $remoteIp,
234@@ -187,6 +210,10 @@ protected function writeBanFile()
235 */
236 public function handleFailedLogin($server)
237 {
238+ if (!$this->lastErrorIsBanishable) {
239+ return $this->lastErrorReason ?: 'Error during login.';
240+ };
241+
242 $ip = $server['REMOTE_ADDR'];
243 $trusted = $this->configManager->get('security.trusted_proxies', []);
244
245@@ -215,6 +242,7 @@ public function handleFailedLogin($server)
246 );
247 }
248 $this->writeBanFile();
249+ return $this->lastErrorReason ?: 'Error during login.';
250 }
251
252 /**
253diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
254index b8b8ab8..5eb4aac 100644
255--- a/application/security/SessionManager.php
256+++ b/application/security/SessionManager.php
257@@ -111,10 +111,10 @@ public static function checkId($sessionId)
258 *
259 * @param string $clientIpId Client IP address identifier
260 */
261- public function storeLoginInfo($clientIpId)
262+ public function storeLoginInfo($clientIpId, $login = null)
263 {
264 $this->session['ip'] = $clientIpId;
265- $this->session['username'] = $this->conf->get('credentials.login');
266+ $this->session['username'] = $login ?: $this->conf->get('credentials.login');
267 $this->extendTimeValidityBy(self::$SHORT_TIMEOUT);
268 }
269
270diff --git a/index.php b/index.php
5752291a 271index 4b86a3e..58ae2dd 100644
95b20e17
IB
272--- a/index.php
273+++ b/index.php
5752291a 274@@ -121,7 +121,32 @@
95b20e17
IB
275 $_COOKIE['shaarli'] = session_id();
276 }
277
278-$conf = new ConfigManager();
279+$folderBase = getenv("BASE");
280+
281+if (getenv("USERSPACE")) {
282+ if (isset($_GET["do"]) && $_GET["do"] == "login") {
283+ header("Location: $folderBase/?do=login");
284+ exit;
285+ }
286+ $userspace = preg_replace("/[^-_A-Za-z0-9]/", '', getenv("USERSPACE"));
287+} else if (isset($_SESSION["username"]) && $_SESSION["username"]) {
288+ header("Location: " . $folderBase . "/" . $_SESSION["username"] . "?");
289+ exit;
290+} else if (!isset($_GET["do"]) || $_GET["do"] != "login") {
291+ header("Location: $folderBase/?do=login");
292+ exit;
293+}
294+
5752291a
IB
295+if (!isset($userspace) && isset($_POST["login"])) {
296+ $userspace = preg_replace("/[^-_A-Za-z0-9]/", '', $_POST["login"]);
297+ error_log("debugImmae: setting userspace from POST: " . $userspace);
298+}
299+
95b20e17
IB
300+if (isset($userspace)) {
301+ $conf = new ConfigManager(null, $userspace);
302+} else {
303+ $conf = new ConfigManager();
304+}
305 $sessionManager = new SessionManager($_SESSION, $conf);
306 $loginManager = new LoginManager($GLOBALS, $conf, $sessionManager);
307 $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
5752291a 308@@ -175,7 +200,7 @@
95b20e17
IB
309 }
310
311 // Display the installation form if no existing config is found
312- install($conf, $sessionManager, $loginManager);
313+ install($conf, $sessionManager, $loginManager, $userspace);
314 }
315
316 $loginManager->checkLoginState($_COOKIE, $clientIpId);
5752291a 317@@ -205,6 +230,7 @@ function isLoggedIn()
95b20e17
IB
318 && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
319 ) {
320 $loginManager->handleSuccessfulLogin($_SERVER);
321+ $userspace = $_POST['login'];
322
323 $cookiedir = '';
324 if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
5752291a 325@@ -241,25 +267,25 @@ function isLoggedIn()
95b20e17
IB
326 $uri .= '&'.$param.'='.urlencode($_GET[$param]);
327 }
328 }
329- header('Location: '. $uri);
330+ header('Location: '. $userspace . $uri);
331 exit;
332 }
333
334 if (isset($_GET['edit_link'])) {
335- header('Location: ?edit_link='. escape($_GET['edit_link']));
336+ header('Location: ' . $userspace . '?edit_link='. escape($_GET['edit_link']));
337 exit;
338 }
339
340 if (isset($_POST['returnurl'])) {
341 // Prevent loops over login screen.
342 if (strpos($_POST['returnurl'], 'do=login') === false) {
343- header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
344+ header('Location: ' . generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
345 exit;
346 }
347 }
348- header('Location: ?'); exit;
349+ header('Location: '. $userspace . '?'); exit;
350 } else {
351- $loginManager->handleFailedLogin($_SERVER);
352+ $errorReason = $loginManager->handleFailedLogin($_SERVER);
353 $redir = '&username='. urlencode($_POST['login']);
354 if (isset($_GET['post'])) {
355 $redir .= '&post=' . urlencode($_GET['post']);
5752291a 356@@ -270,7 +296,7 @@ function isLoggedIn()
95b20e17
IB
357 }
358 }
359 // Redirect to login screen.
360- echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
361+ echo '<script>alert("'. t($errorReason) .'");document.location=\'?do=login'.$redir.'\';</script>';
362 exit;
363 }
364 }
5752291a 365@@ -1719,7 +1745,7 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
95b20e17
IB
366 * @param SessionManager $sessionManager SessionManager instance
367 * @param LoginManager $loginManager LoginManager instance
368 */
369-function install($conf, $sessionManager, $loginManager) {
370+function install($conf, $sessionManager, $loginManager, $userspace) {
371 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
372 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
373
5752291a 374@@ -1755,7 +1781,7 @@ function install($conf, $sessionManager, $loginManager) {
95b20e17
IB
375 }
376
377
378- if (!empty($_POST['setlogin']) && !empty($_POST['setpassword']))
379+ if (true)
380 {
381 $tz = 'UTC';
382 if (!empty($_POST['continent']) && !empty($_POST['city'])
5752291a 383@@ -1764,15 +1790,15 @@ function install($conf, $sessionManager, $loginManager) {
95b20e17
IB
384 $tz = $_POST['continent'].'/'.$_POST['city'];
385 }
386 $conf->set('general.timezone', $tz);
387- $login = $_POST['setlogin'];
388- $conf->set('credentials.login', $login);
389+ $conf->set('credentials.login', $userspace);
390 $salt = sha1(uniqid('', true) .'_'. mt_rand());
391 $conf->set('credentials.salt', $salt);
392- $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
393+ $hash = sha1(uniqid('', true) .'_'. mt_rand());
394+ $conf->set('credentials.hash', $hash);
395 if (!empty($_POST['title'])) {
396 $conf->set('general.title', escape($_POST['title']));
397 } else {
398- $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
399+ $conf->set('general.title', ucwords(str_replace("_", " ", $userspace)));
400 }
401 $conf->set('translation.language', escape($_POST['language']));
402 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
5752291a 403@@ -1841,7 +1867,12 @@ function install($conf, $sessionManager, $loginManager) {
95b20e17
IB
404 $app = new \Slim\App($container);
405
406 // REST API routes
407-$app->group('/api/v1', function() {
408+if (isset($userspace)) {
409+ $mountpoint = '/' . $userspace . '/api/v1';
410+} else {
411+ $mountpoint = '/api/v1';
412+}
413+$app->group($mountpoint, function() {
414 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
415 $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
416 $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
5752291a 417@@ -1860,7 +1891,7 @@ function install($conf, $sessionManager, $loginManager) {
95b20e17
IB
418 $response = $app->run(true);
419 // Hack to make Slim and Shaarli router work together:
420 // If a Slim route isn't found and NOT API call, we call renderPage().
421-if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
422+if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], $mountpoint) === false) {
423 // We use UTF-8 for proper international characters handling.
424 header('Content-Type: text/html; charset=utf-8');
425 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);