1<?php 2/** 3 * EGroupware API: session handling 4 * 5 * This class is based on the old phpgwapi/inc/class.sessions(_php4).inc.php: 6 * (c) 1998-2000 NetUSE AG Boris Erdmann, Kristian Koehntopp 7 * (c) 2003 FreeSoftware Foundation 8 * Not sure how much the current code still has to do with it. 9 * 10 * Former authers were: 11 * - NetUSE AG Boris Erdmann, Kristian Koehntopp 12 * - Dan Kuykendall <seek3r@phpgroupware.org> 13 * - Joseph Engo <jengo@phpgroupware.org> 14 * 15 * @link http://www.egroupware.org 16 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 17 * @package api 18 * @subpackage session 19 * @author Ralf Becker <ralfbecker@outdoor-training.de> since 2003 on 20 */ 21 22namespace EGroupware\Api; 23 24use PragmaRX\Google2FA; 25use EGroupware\Api\Mail\Credentials; 26use EGroupware\OpenID; 27use League\OAuth2\Server\Exception\OAuthServerException; 28 29/** 30 * Create, verifies or destroys an EGroupware session 31 * 32 * If you want to analyse the memory usage in the session, you can uncomment the following call: 33 * 34 * static function encrypt($kp3) 35 * { 36 * // switch that on to analyse memory usage in the session 37 * //self::log_session_usage($_SESSION[self::EGW_APPSESSION_VAR],'_SESSION['.self::EGW_APPSESSION_VAR.']',true,5000); 38 */ 39class Session 40{ 41 /** 42 * Write debug messages about session verification and creation to the error_log 43 * 44 * This will contain passwords! Don't leave it permanently switched on! 45 */ 46 const ERROR_LOG_DEBUG = false; 47 48 /** 49 * key of eGW's session-data in $_SESSION 50 */ 51 const EGW_SESSION_VAR = 'egw_session'; 52 53 /** 54 * key of eGW's application session-data in $_SESSION 55 */ 56 const EGW_APPSESSION_VAR = 'egw_app_session'; 57 58 /** 59 * key of eGW's required files in $_SESSION 60 * 61 * These files get set by Db and Egw class, for classes which get not autoloaded (eg. ADOdb, idots_framework) 62 */ 63 const EGW_REQUIRED_FILES = 'egw_required_files'; 64 65 /** 66 * key of eGW's egw_info cached in $_SESSION 67 */ 68 const EGW_INFO_CACHE = 'egw_info_cache'; 69 70 /** 71 * key of eGW's egw object cached in $_SESSION 72 */ 73 const EGW_OBJECT_CACHE = 'egw_object_cache'; 74 75 /** 76 * Name of cookie or get-parameter with session-id 77 */ 78 const EGW_SESSION_NAME = 'sessionid'; 79 80 /** 81 * Name of cookie with remember me token 82 */ 83 const REMEMBER_ME_COOKIE = 'eGW_remember'; 84 85 /** 86 * current user login (account_lid@domain) 87 * 88 * @var string 89 */ 90 var $login; 91 92 /** 93 * current user password 94 * 95 * @var string 96 */ 97 var $passwd; 98 99 /** 100 * current user db/ldap account id 101 * 102 * @var int 103 */ 104 var $account_id; 105 106 /** 107 * current user account login id (without the eGW-domain/-instance part 108 * 109 * @var string 110 */ 111 var $account_lid; 112 113 /** 114 * domain for current user 115 * 116 * @var string 117 */ 118 var $account_domain; 119 120 /** 121 * type flag, A - anonymous session, N - None, normal session 122 * 123 * @var string 124 */ 125 var $session_flags; 126 127 /** 128 * current user session id 129 * 130 * @var string 131 */ 132 var $sessionid; 133 134 /** 135 * an other session specific id (md5 from a random string), 136 * used together with the sessionid for xmlrpc basic auth and the encryption of session-data (if that's enabled) 137 * 138 * @var string 139 */ 140 var $kp3; 141 142 /** 143 * Primary key of egw_access_log row for updates 144 * 145 * @var int 146 */ 147 var $sessionid_access_log; 148 149 /** 150 * name of XML-RPC/SOAP method called 151 * 152 * @var string 153 */ 154 var $xmlrpc_method_called; 155 156 /** 157 * Array with the name of the system domains 158 * 159 * @var array 160 */ 161 private $egw_domains; 162 163 /** 164 * $_SESSION at the time the constructor was called 165 * 166 * @var array 167 */ 168 var $required_files; 169 170 /** 171 * Nummeric code why session creation failed 172 * 173 * @var int 174 */ 175 var $cd_reason; 176 const CD_BAD_LOGIN_OR_PASSWORD = 5; 177 const CD_SECOND_FACTOR_REQUIRED = 96; 178 const CD_FORCE_PASSWORD_CHANGE = 97; 179 const CD_ACCOUNT_EXPIRED = 98; 180 const CD_BLOCKED = 99; // to many failed attempts to loing 181 182 /** 183 * Verbose reason why session creation failed 184 * 185 * @var string 186 */ 187 var $reason; 188 189 /** 190 * Session action set by update_dla or set_action and stored in __destruct 191 * 192 * @var string 193 */ 194 protected $action; 195 196 /** 197 * Constructor just loads up some defaults from cookies 198 * 199 * @param array $domain_names =null domain-names used in this install 200 */ 201 function __construct(array $domain_names=null) 202 { 203 $this->required_files = $_SESSION[self::EGW_REQUIRED_FILES]; 204 205 $this->sessionid = self::get_sessionid(); 206 $this->kp3 = self::get_request('kp3'); 207 208 $this->egw_domains = $domain_names; 209 210 if (!isset($GLOBALS['egw_setup'])) 211 { 212 // verfiy and if necessary create and save our config settings 213 // 214 $save_rep = false; 215 if (!isset($GLOBALS['egw_info']['server']['max_access_log_age'])) 216 { 217 $GLOBALS['egw_info']['server']['max_access_log_age'] = 90; // default 90 days 218 $save_rep = true; 219 } 220 if (!isset($GLOBALS['egw_info']['server']['block_time'])) 221 { 222 $GLOBALS['egw_info']['server']['block_time'] = 1; // default 1min, its enough to slow down brute force attacks 223 $save_rep = true; 224 } 225 if (!isset($GLOBALS['egw_info']['server']['num_unsuccessful_id'])) 226 { 227 $GLOBALS['egw_info']['server']['num_unsuccessful_id'] = 3; // default 3 trys per id 228 $save_rep = true; 229 } 230 if (!isset($GLOBALS['egw_info']['server']['num_unsuccessful_ip'])) 231 { 232 $GLOBALS['egw_info']['server']['num_unsuccessful_ip'] = $GLOBALS['egw_info']['server']['num_unsuccessful_id'] * 5; // default is 5 times as high as the id default; since accessing via proxy is quite common 233 $save_rep = true; 234 } 235 if (!isset($GLOBALS['egw_info']['server']['install_id'])) 236 { 237 $GLOBALS['egw_info']['server']['install_id'] = md5(Auth::randomstring(15)); 238 } 239 if (!isset($GLOBALS['egw_info']['server']['max_history'])) 240 { 241 $GLOBALS['egw_info']['server']['max_history'] = 20; 242 $save_rep = true; 243 } 244 245 if ($save_rep) 246 { 247 $config = new Config('phpgwapi'); 248 $config->read_repository(); 249 $config->value('max_access_log_age',$GLOBALS['egw_info']['server']['max_access_log_age']); 250 $config->value('block_time',$GLOBALS['egw_info']['server']['block_time']); 251 $config->value('num_unsuccessful_id',$GLOBALS['egw_info']['server']['num_unsuccessful_id']); 252 $config->value('num_unsuccessful_ip',$GLOBALS['egw_info']['server']['num_unsuccessful_ip']); 253 $config->value('install_id',$GLOBALS['egw_info']['server']['install_id']); 254 $config->value('max_history',$GLOBALS['egw_info']['server']['max_history']); 255 try 256 { 257 $config->save_repository(); 258 } 259 catch (Db\Exception $e) { 260 _egw_log_exception($e); // ignore exception, as it blocks session creation, if database is not writable 261 } 262 } 263 } 264 self::set_cookiedomain(); 265 266 // set session_timeout from global php.ini and default to 14400=4h, if not set 267 if (!($GLOBALS['egw_info']['server']['sessions_timeout'] = ini_get('session.gc_maxlifetime'))) 268 { 269 ini_set('session.gc_maxlifetime', $GLOBALS['egw_info']['server']['sessions_timeout']=14400); 270 } 271 } 272 273 /** 274 * Magic function called when this class get's restored from the session 275 * 276 */ 277 function __wakeup() 278 { 279 if (!empty($GLOBALS['egw_info']['server']['sessions_timeout']) && session_status() === PHP_SESSION_NONE) 280 { 281 ini_set('session.gc_maxlifetime', $GLOBALS['egw_info']['server']['sessions_timeout']); 282 } 283 $this->action = null; 284 } 285 286 /** 287 * Destructor: update access-log and encrypt session 288 */ 289 function __destruct() 290 { 291 self::encrypt($this->kp3); 292 } 293 294 /** 295 * commit the sessiondata to storage 296 * 297 * It's necessary to use this function instead of session_write_close() direct, as otherwise the session is not encrypted! 298 */ 299 function commit_session() 300 { 301 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() sessionid=$this->sessionid, _SESSION[".self::EGW_SESSION_VAR.']='.array2string($_SESSION[self::EGW_SESSION_VAR]).' '.function_backtrace()); 302 self::encrypt($this->kp3); 303 304 session_write_close(); 305 } 306 307 /** 308 * Keys of session variables which get encrypted 309 * 310 * @var array 311 */ 312 static $egw_session_vars = array( 313 //self::EGW_SESSION_VAR, no need to encrypt and required by the session list 314 self::EGW_APPSESSION_VAR, 315 self::EGW_INFO_CACHE, 316 self::EGW_OBJECT_CACHE, 317 ); 318 319 static $mcrypt; 320 321 /** 322 * Name of flag in session to signal it is encrypted or not 323 */ 324 const EGW_SESSION_ENCRYPTED = 'egw_session_encrypted'; 325 326 /** 327 * Encrypt the variables in the session 328 * 329 * Is called by self::__destruct(). 330 */ 331 static function encrypt($kp3) 332 { 333 // switch that on to analyse memory usage in the session 334 //self::log_session_usage($_SESSION[self::EGW_APPSESSION_VAR],'_SESSION['.self::EGW_APPSESSION_VAR.']',true,5000); 335 336 if (!isset($_SESSION[self::EGW_SESSION_ENCRYPTED]) && self::init_crypt($kp3)) 337 { 338 foreach(self::$egw_session_vars as $name) 339 { 340 if (isset($_SESSION[$name])) 341 { 342 $_SESSION[$name] = mcrypt_generic(self::$mcrypt,serialize($_SESSION[$name])); 343 //error_log(__METHOD__."() 'encrypting' session var: $name, len=".strlen($_SESSION[$name])); 344 } 345 } 346 $_SESSION[self::EGW_SESSION_ENCRYPTED] = true; // flag session as encrypted 347 348 mcrypt_generic_deinit(self::$mcrypt); 349 self::$mcrypt = null; 350 } 351 } 352 353 /** 354 * Log the usage of session-vars 355 * 356 * @param array &$arr 357 * @param string $label 358 * @param boolean $recursion =true if true call itself for every item > $limit 359 * @param int $limit =1000 log only differences > $limit 360 */ 361 static function log_session_usage(&$arr,$label,$recursion=true,$limit=1000) 362 { 363 if (!is_array($arr)) return; 364 365 $sizes = array(); 366 foreach($arr as $key => &$data) 367 { 368 $sizes[$key] = strlen(serialize($data)); 369 } 370 arsort($sizes,SORT_NUMERIC); 371 foreach($sizes as $key => $size) 372 { 373 $diff = $size - (int)$_SESSION[$label.'-sizes'][$key]; 374 $_SESSION[$label.'-sizes'][$key] = $size; 375 if ($diff > $limit) 376 { 377 error_log("strlen({$label}[$key])=".Vfs::hsize($size).", diff=".Vfs::hsize($diff)); 378 if ($recursion) self::log_session_usage($arr[$key],$label.'['.$key.']',$recursion,$limit); 379 } 380 } 381 } 382 383 /** 384 * Decrypt the variables in the session 385 * 386 * Is called by self::init_handler from api/src/loader.php (called from the header.inc.php) 387 * before the restore of the eGW enviroment takes place, so that the whole thing can be encrypted 388 */ 389 static function decrypt() 390 { 391 if ($_SESSION[self::EGW_SESSION_ENCRYPTED] && self::init_crypt(self::get_request('kp3'))) 392 { 393 foreach(self::$egw_session_vars as $name) 394 { 395 if (isset($_SESSION[$name])) 396 { 397 $_SESSION[$name] = unserialize(trim(mdecrypt_generic(self::$mcrypt,$_SESSION[$name]))); 398 //error_log(__METHOD__."() 'decrypting' session var $name: gettype($name) = ".gettype($_SESSION[$name])); 399 } 400 } 401 unset($_SESSION[self::EGW_SESSION_ENCRYPTED]); // delete encryption flag 402 } 403 } 404 405 /** 406 * Check if session encryption is configured, possible and initialise it 407 * 408 * If mcrypt extension is not available (eg. in PHP 7.2+ no longer contains it) fail gracefully. 409 * 410 * @param string $kp3 mcrypt key transported via cookie or get parameter like the session id, 411 * unlike the session id it's not know on the server, so only the client-request can decrypt the session! 412 * @return boolean true if encryption is used, false otherwise 413 */ 414 static private function init_crypt($kp3) 415 { 416 if(!$GLOBALS['egw_info']['server']['mcrypt_enabled']) 417 { 418 return false; // session encryption is switched off 419 } 420 if ($GLOBALS['egw_info']['currentapp'] == 'syncml' || !$kp3) 421 { 422 $kp3 = 'staticsyncmlkp3'; // syncml has no kp3! 423 } 424 if (is_null(self::$mcrypt)) 425 { 426 if (!check_load_extension('mcrypt')) 427 { 428 error_log(__METHOD__."() required PHP extension mcrypt not loaded and can not be loaded, sessions get NOT encrypted!"); 429 return false; 430 } 431 if (!(self::$mcrypt = mcrypt_module_open(MCRYPT_TRIPLEDES, '', MCRYPT_MODE_ECB, ''))) 432 { 433 error_log(__METHOD__."() could not mcrypt_module_open(MCRYPT_TRIPLEDES,'',MCRYPT_MODE_ECB,''), sessions get NOT encrypted!"); 434 return false; 435 } 436 $iv_size = mcrypt_enc_get_iv_size(self::$mcrypt); 437 $iv = !isset($GLOBALS['egw_info']['server']['mcrypt_iv']) || strlen($GLOBALS['egw_info']['server']['mcrypt_iv']) < $iv_size ? 438 mcrypt_create_iv ($iv_size, MCRYPT_RAND) : substr($GLOBALS['egw_info']['server']['mcrypt_iv'],0,$iv_size); 439 440 if (mcrypt_generic_init(self::$mcrypt,$kp3, $iv) < 0) 441 { 442 error_log(__METHOD__."() could not initialise mcrypt, sessions get NOT encrypted!"); 443 return self::$mcrypt = false; 444 } 445 } 446 return is_resource(self::$mcrypt); 447 } 448 449 /** 450 * Create a new eGW session 451 * 452 * @param string $login user login 453 * @param string $passwd user password 454 * @param string $passwd_type type of password being used, ie plaintext, md5, sha1 455 * @param boolean $no_session =false dont create a real session, eg. for GroupDAV clients using only basic auth, no cookie support 456 * @param boolean $auth_check =true if false, the user is loged in without checking his password (eg. for single sign on), default = true 457 * @param boolean $fail_on_forced_password_change =false true: do NOT create session, if password change requested 458 * @param string|boolean $check_2fa =false string: 2fa-code to check (only if exists) and fail if wrong, false: do NOT check 2fa 459 * @param string $remember_me =null "True" for checkbox checked, or periode for user-choice select-box eg. "P1W" or "" for NOT remember 460 * @return string|boolean session id or false if session was not created, $this->(cd_)reason contains cause 461 */ 462 function create($login,$passwd = '',$passwd_type = '',$no_session=false,$auth_check=true,$fail_on_forced_password_change=false,$check_2fa=false,$remember_me=null) 463 { 464 try { 465 if (is_array($login)) 466 { 467 $this->login = $login['login']; 468 $this->passwd = $login['passwd']; 469 $this->passwd_type = $login['passwd_type']; 470 $login = $this->login; 471 } 472 else 473 { 474 $this->login = $login; 475 $this->passwd = $passwd; 476 $this->passwd_type = $passwd_type; 477 } 478 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) starting ..."); 479 480 self::split_login_domain($login,$this->account_lid,$this->account_domain); 481 // add domain to the login, if not already there 482 if (substr($this->login,-strlen($this->account_domain)-1) != '@'.$this->account_domain) 483 { 484 $this->login .= '@'.$this->account_domain; 485 } 486 $now = time(); 487 //error_log(__METHOD__."($login,$passwd,$passwd_type,$no_session,$auth_check) account_lid=$this->account_lid, account_domain=$this->account_domain, default_domain={$GLOBALS['egw_info']['server']['default_domain']}, user/domain={$GLOBALS['egw_info']['user']['domain']}"); 488 489 // This is to ensure that we authenticate to the correct domain (might not be default) 490 // if no domain is given we use the default domain, so we dont need to re-create everything 491 if (!$GLOBALS['egw_info']['user']['domain'] && $this->account_domain == $GLOBALS['egw_info']['server']['default_domain']) 492 { 493 $GLOBALS['egw_info']['user']['domain'] = $this->account_domain; 494 } 495 elseif (!$this->account_domain && $GLOBALS['egw_info']['user']['domain']) 496 { 497 $this->account_domain = $GLOBALS['egw_info']['user']['domain']; 498 } 499 elseif($this->account_domain != $GLOBALS['egw_info']['user']['domain']) 500 { 501 throw new Exception("Wrong domain! '$this->account_domain' != '{$GLOBALS['egw_info']['user']['domain']}'"); 502 } 503 unset($GLOBALS['egw_info']['server']['default_domain']); // we kill this for security reasons 504 505 $user_ip = self::getuser_ip(); 506 507 $this->account_id = $GLOBALS['egw']->accounts->name2id($this->account_lid,'account_lid','u'); 508 509 // do we need to check 'remember me' token (to bypass authentication) 510 if ($auth_check && !empty($_COOKIE[self::REMEMBER_ME_COOKIE])) 511 { 512 $auth_check = !$this->skipPasswordAuth($_COOKIE[self::REMEMBER_ME_COOKIE], $this->account_id); 513 } 514 515 if (($blocked = $this->login_blocked($login,$user_ip)) || // too many unsuccessful attempts 516 $GLOBALS['egw_info']['server']['global_denied_users'][$this->account_lid] || 517 $auth_check && !$GLOBALS['egw']->auth->authenticate($this->account_lid, $this->passwd, $this->passwd_type) || 518 $this->account_id && $GLOBALS['egw']->accounts->get_type($this->account_id) == 'g') 519 { 520 $this->reason = $blocked ? 'blocked, too many attempts' : 'bad login or password'; 521 $this->cd_reason = $blocked ? self::CD_BLOCKED : self::CD_BAD_LOGIN_OR_PASSWORD; 522 523 // we dont log anon users as it would block the website 524 if (!$GLOBALS['egw']->acl->get_specific_rights_for_account($this->account_id,'anonymous','phpgwapi')) 525 { 526 $this->log_access($this->reason,$login,$user_ip,0); // log unsuccessfull login 527 } 528 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)"); 529 return false; 530 } 531 532 if (!$this->account_id && $GLOBALS['egw_info']['server']['auto_create_acct']) 533 { 534 if ($GLOBALS['egw_info']['server']['auto_create_acct'] == 'lowercase') 535 { 536 $this->account_lid = strtolower($this->account_lid); 537 } 538 $this->account_id = $GLOBALS['egw']->accounts->auto_add($this->account_lid, $passwd); 539 } 540 // fix maybe wrong case in username, it makes problems eg. in filemanager (name of homedir) 541 if ($this->account_lid != ($lid = $GLOBALS['egw']->accounts->id2name($this->account_id))) 542 { 543 $this->account_lid = $lid; 544 $this->login = $lid.substr($this->login,strlen($lid)); 545 } 546 547 $GLOBALS['egw_info']['user']['account_id'] = $this->account_id; 548 549 // for *DAV and eSync we use a pseudo sessionid created from md5(user:passwd) 550 // --> allows this stateless protocolls which use basic auth to use sessions! 551 if (($this->sessionid = self::get_sessionid(true))) 552 { 553 if (session_status() !== PHP_SESSION_ACTIVE) // gives warning including password 554 { 555 session_id($this->sessionid); 556 } 557 } 558 elseif (!headers_sent()) // only gives warnings, nothing we can do 559 { 560 self::cache_control(); 561 session_start(); 562 // set a new session-id, if not syncml (already done in Horde code and can NOT be changed) 563 if (!$no_session && $GLOBALS['egw_info']['flags']['currentapp'] != 'syncml') 564 { 565 session_regenerate_id(true); 566 } 567 $this->sessionid = session_id(); 568 } 569 else 570 { 571 $this->sessionid = session_id() ?: Auth::randomstring(24); 572 } 573 $this->kp3 = Auth::randomstring(24); 574 575 $GLOBALS['egw_info']['user'] = $this->read_repositories(); 576 if ($GLOBALS['egw']->accounts->is_expired($GLOBALS['egw_info']['user'])) 577 { 578 $this->reason = 'account is expired'; 579 $this->cd_reason = self::CD_ACCOUNT_EXPIRED; 580 581 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)"); 582 return false; 583 } 584 585 Cache::setSession('phpgwapi', 'password', base64_encode($this->passwd)); 586 587 // if we have a second factor, check it before forced password change 588 if ($check_2fa !== false) 589 { 590 try { 591 $this->checkMultifactorAuth($check_2fa, $_COOKIE[self::REMEMBER_ME_COOKIE]); 592 } 593 catch(\Exception $e) { 594 $this->cd_reason = $e->getCode(); 595 $this->reason = $e->getMessage(); 596 $this->log_access($this->reason, $login, $user_ip, 0); // log unsuccessfull login 597 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check,$fail_on_forced_password_change,'$check_2fa') UNSUCCESSFULL ($this->reason)"); 598 return false; 599 } 600 } 601 602 if ($fail_on_forced_password_change && Auth::check_password_change($this->reason) === false) 603 { 604 $this->cd_reason = self::CD_FORCE_PASSWORD_CHANGE; 605 return false; 606 } 607 608 if ($GLOBALS['egw']->acl->check('anonymous',1,'phpgwapi')) 609 { 610 $this->session_flags = 'A'; 611 } 612 else 613 { 614 $this->session_flags = 'N'; 615 } 616 617 if (($hook_result = Hooks::process(array( 618 'location' => 'session_creation', 619 'sessionid' => $this->sessionid, 620 'session_flags' => $this->session_flags, 621 'account_id' => $this->account_id, 622 'account_lid' => $this->account_lid, 623 'passwd' => $this->passwd, 624 'account_domain' => $this->account_domain, 625 'user_ip' => $user_ip, 626 ),'',true))) // true = run hooks from all apps, not just the ones the current user has perms to run 627 { 628 foreach($hook_result as $reason) 629 { 630 if ($reason) // called hook requests to deny the session 631 { 632 $this->reason = $this->cd_reason = $reason; 633 $this->log_access($this->reason,$login,$user_ip,0); // log unsuccessfull login 634 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)"); 635 return false; 636 } 637 } 638 } 639 $GLOBALS['egw']->db->transaction_begin(); 640 $this->register_session($this->login,$user_ip,$now,$this->session_flags); 641 if ($this->session_flags != 'A') // dont log anonymous sessions 642 { 643 $this->sessionid_access_log = $this->log_access($this->sessionid,$login,$user_ip,$this->account_id); 644 // We do NOT log anonymous sessions to not block website and also to cope with 645 // high rate anon endpoints might be called creating a bottleneck in the egw_accounts table. 646 Cache::setSession('phpgwapi', 'account_previous_login', $GLOBALS['egw']->auth->previous_login); 647 $GLOBALS['egw']->accounts->update_lastlogin($this->account_id,$user_ip); 648 } 649 $GLOBALS['egw']->db->transaction_commit(); 650 651 if (!headers_sent()) 652 { 653 if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session) 654 { 655 self::egw_setcookie(self::EGW_SESSION_NAME, $this->sessionid); 656 self::egw_setcookie('kp3', $this->kp3); 657 self::egw_setcookie('domain', $this->account_domain); 658 } 659 if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session || isset($_COOKIE['last_loginid'])) 660 { 661 self::egw_setcookie('last_loginid', $this->account_lid, $now + 1209600); /* For 2 weeks */ 662 self::egw_setcookie('last_domain', $this->account_domain, $now + 1209600); 663 } 664 665 // set new remember me token/cookie, if requested and necessary 666 $expiration = null; 667 if (($token = $this->checkSetRememberMeToken($remember_me, $_COOKIE[self::REMEMBER_ME_COOKIE], $expiration))) 668 { 669 self::egw_setcookie(self::REMEMBER_ME_COOKIE, $token, $expiration); 670 } 671 672 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__ . "($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) successfull sessionid=$this->sessionid"); 673 } 674 elseif (self::ERROR_LOG_DEBUG) error_log(__METHOD__ . "($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) could NOT set session cookies, headers already sent"); 675 676 // hook called once session is created 677 Hooks::process(array( 678 'location' => 'session_created', 679 'sessionid' => $this->sessionid, 680 'session_flags' => $this->session_flags, 681 'account_id' => $this->account_id, 682 'account_lid' => $this->account_lid, 683 'passwd' => $this->passwd, 684 'account_domain' => $this->account_domain, 685 'user_ip' => $user_ip, 686 'session_type' => Session\Type::get($_SERVER['REQUEST_URI'], 687 $GLOBALS['egw_info']['flags']['current_app'], 688 true), // true return WebGUI instead of login, as we are logged in now 689 ),'',true); 690 691 return $this->sessionid; 692 } 693 // catch all exceptions, as their (allways logged) trace (eg. on a database error) would contain the user password 694 catch(Exception $e) { 695 $this->reason = $this->cd_reason = is_a($e, Db\Exception::class) ? 696 // do not output specific database error, eg. invalid SQL statement 697 lang('Database Error!') : $e->getMessage(); 698 error_log(__METHOD__."('$login', ".array2string(str_repeat('*', strlen($passwd))). 699 ", '$passwd_type', no_session=".array2string($no_session). 700 ", auth_check=".array2string($auth_check). 701 ", fail_on_forced_password_change=".array2string($fail_on_forced_password_change). 702 ") Exception ".$e->getMessage()); 703 return false; 704 } 705 } 706 707 /** 708 * Check if password authentication is required or given token is sufficient 709 * 710 * Token is only checked for 'remember_me_token' === 'always', not for default of only for 2FA! 711 * 712 * Password auth is also required if 2FA is not disabled and either required or configured by user. 713 * 714 * @param string $token value of token 715 * @param int& $account_id =null account_id of token-owner to limit check on that user, on return account_id of token owner 716 * @return boolean false: if further auth check is required, true: if token is sufficient for authentication 717 */ 718 public function skipPasswordAuth($token, &$account_id=null) 719 { 720 // if token is empty or disabled --> password authentication required 721 if (empty($token) || $GLOBALS['egw_info']['server']['remember_me_token'] !== 'always' || 722 !($client = $this->checkOpenIDconfigured())) 723 { 724 return false; 725 } 726 727 // check if token exists and is (still) valid 728 $tokenRepo = new OpenID\Repositories\AccessTokenRepository(); 729 if (!($access_token = $tokenRepo->findToken($client, $account_id, 'PT1S', $token))) 730 { 731 return false; 732 } 733 $account_id = $access_token->getUserIdentifier(); 734 735 // check if we need a second factor 736 if ($GLOBALS['egw_info']['server']['2fa_required'] !== 'disabled' && 737 (($creds = Credentials::read(0, Credentials::TWOFA, $account_id)) || 738 $GLOBALS['egw_info']['server']['2fa_required'] === 'strict')) 739 { 740 return false; 741 } 742 743 // access-token is sufficient 744 return true; 745 } 746 747 /** 748 * Check multifcator authemtication 749 * 750 * @param string $code 2fa-code 751 * @param string $token remember me token 752 * @throws \Exception with error-message if NOT successful 753 */ 754 protected function checkMultifactorAuth($code, $token) 755 { 756 $errors = $factors = []; 757 758 if ($GLOBALS['egw_info']['server']['2fa_required'] === 'disabled') 759 { 760 return; // nothing to check 761 } 762 763 // check if token exists and is (still) valid 764 if (!empty($token) && $GLOBALS['egw_info']['server']['remember_me_token'] !== 'disabled' && 765 ($client = $this->checkOpenIDconfigured())) 766 { 767 $tokenRepo = new OpenID\Repositories\AccessTokenRepository(); 768 if ($tokenRepo->findToken($client, $this->account_id, 'PT1S', $token)) 769 { 770 $factors['remember_me_token'] = true; 771 } 772 else 773 { 774 $errors['remember_me_token'] = lang("Invalid or expired 'remember me' token"); 775 } 776 } 777 778 // if 2fa is configured by user, check it 779 if (($creds = Credentials::read(0, Credentials::TWOFA, $this->account_id))) 780 { 781 if (empty($code)) 782 { 783 $errors['2fa_code'] = lang('2-Factor Authentication code required'); 784 } 785 else 786 { 787 $google2fa = new Google2FA\Google2FA(); 788 if (!empty($code) && $google2fa->verify($code, $creds['2fa_password'])) 789 { 790 $factors['2fa_code'] = true; 791 } 792 else 793 { 794 $errors['2fa_code'] = lang('Invalid 2-Factor Authentication code'); 795 } 796 } 797 } 798 799 // check for more factors and/or policies 800 // hook can add factors, errors or throw \Exception with error-message and -code 801 Hooks::process([ 802 'location' => 'multifactor_policy', 803 'factors' => &$factors, 804 'errors' => &$errors, 805 '2fa_code' => $code, 806 'remember_me_token' => $token, 807 ], [], true); 808 809 if (!count($factors) && (count($errors) || 810 $GLOBALS['egw_info']['server']['2fa_required'] === 'strict')) 811 { 812 if (!empty($code) && isset($errors['2fa_code'])) 813 { 814 // we log the missing factor, but externally only show "Bad Login or Password" 815 // to give no indication that the password was already correct 816 throw new \Exception(implode(', ', $errors), self::CD_BAD_LOGIN_OR_PASSWORD); 817 } 818 else 819 { 820 throw new \Exception(implode(', ', $errors), self::CD_SECOND_FACTOR_REQUIRED); 821 } 822 } 823 } 824 825 /** 826 * Check if we need to set a remember me token/cookie 827 * 828 * @param string $remember_me =null "True" for checkbox checked, or periode for user-choice select-box eg. "P1W" or "" for NOT remember 829 * @param string $token current remember me token 830 * @param int& $expriation on return expiration time of new cookie 831 * @return string new token to set as Cookieor null to not set a new one 832 */ 833 protected function checkSetRememberMeToken($remember_me, $token, &$expiration) 834 { 835 // do we need a new token 836 if (!empty($remember_me) && $GLOBALS['egw_info']['server']['remember_me_token'] !== 'disabled' && 837 ($client = $this->checkOpenIDconfigured())) 838 { 839 if (!empty($token)) 840 { 841 // check if token exists and is (still) valid 842 $tokenRepo = new OpenID\Repositories\AccessTokenRepository(); 843 if ($tokenRepo->findToken($client, $this->account_id, 'PT1S', $token)) 844 { 845 return null; // token still valid, no need to set it again 846 } 847 } 848 $lifetime = $this->rememberMeTokenLifetime(is_string($remember_me) ? $remember_me : null); 849 $expiration = $this->rememberMeTokenLifetime(is_string($remember_me) ? $remember_me : null, true); 850 851 $tokenFactory = new OpenID\Token(); 852 if (($token = $tokenFactory->accessToken(self::OPENID_REMEMBER_ME_CLIENT_ID, [], $lifetime, false, $lifetime, false))) 853 { 854 return $token->getIdentifier(); 855 } 856 } 857 return null; 858 } 859 860 /** 861 * Check if 'remember me' token should be deleted on explict logout 862 * 863 * @return boolean false: if 2FA is enabeld for user, true: otherwise 864 */ 865 public function removeRememberMeTokenOnLogout() 866 { 867 return $GLOBALS['egw_info']['server']['2fa_required'] === 'disabled' || 868 $GLOBALS['egw_info']['server']['2fa_required'] !== 'strict' && 869 !($creds = Credentials::read(0, Credentials::TWOFA, $this->account_id)); 870 } 871 872 /** 873 * OpenID Client ID for remember me token 874 */ 875 const OPENID_REMEMBER_ME_CLIENT_ID = 'login-remember-me'; 876 877 /** 878 * Check and if not configure OpenID app to generate 'remember me' tokens 879 * 880 * @return OpenID\Entities\ClientEntity|null null if OpenID Server app is not installed 881 */ 882 protected function checkOpenIDconfigured() 883 { 884 // OpenID app not installed --> password authentication required 885 if (!isset($GLOBALS['egw_info']['apps'])) 886 { 887 $GLOBALS['egw']->applications->read_installed_apps(); 888 } 889 if (empty($GLOBALS['egw_info']['apps']['openid'])) 890 { 891 return null; 892 } 893 894 $clients = new OpenID\Repositories\ClientRepository(); 895 try { 896 $client = $clients->getClientEntity(self::OPENID_REMEMBER_ME_CLIENT_ID, null, null, false); // false = do NOT check client-secret 897 } 898 catch (OAuthServerException $e) 899 { 900 unset($e); 901 $client = new OpenID\Entities\ClientEntity(); 902 $client->setIdentifier(self::OPENID_REMEMBER_ME_CLIENT_ID); 903 $client->setSecret(Auth::randomstring(24)); // must not be unset 904 $client->setName(lang('Remember me token')); 905 $client->setAccessTokenTTL($this->rememberMeTokenLifetime()); 906 $client->setRefreshTokenTTL('P0S'); // no refresh token 907 $client->setRedirectUri($GLOBALS['egw_info']['server']['webserver_url'].'/'); 908 $clients->persistNewClient($client); 909 } 910 return $client; 911 } 912 913 /** 914 * Return lifetime for remember me token 915 * 916 * @param string $user user choice, if allowed 917 * @param boolean $ts =false false: return periode string, true: return integer timestamp 918 * @return string periode spec eg. 'P1M' 919 */ 920 protected function rememberMeTokenLifetime($user=null, $ts=false) 921 { 922 switch ((string)$GLOBALS['egw_info']['server']['remember_me_lifetime']) 923 { 924 case 'user': 925 if (!empty($user)) 926 { 927 $lifetime = $user; 928 break; 929 } 930 // fall-through for default lifetime 931 case '': // default lifetime 932 $lifetime = 'P1M'; 933 break; 934 default: 935 $lifetime = $GLOBALS['egw_info']['server']['remember_me_lifetime']; 936 break; 937 } 938 if ($ts) 939 { 940 $expiration = new DateTime('now', DateTime::$server_timezone); 941 $expiration->add(new \DateInterval($lifetime)); 942 return $expiration->format('ts'); 943 } 944 return $lifetime; 945 } 946 947 /** 948 * Store eGW specific session-vars 949 * 950 * @param string $login 951 * @param string $user_ip 952 * @param int $now 953 * @param string $session_flags 954 */ 955 private function register_session($login,$user_ip,$now,$session_flags) 956 { 957 // restore session vars set before session was started 958 if (is_array($this->required_files)) 959 { 960 $_SESSION[self::EGW_REQUIRED_FILES] = !is_array($_SESSION[self::EGW_REQUIRED_FILES]) ? $this->required_files : 961 array_unique(array_merge($_SESSION[self::EGW_REQUIRED_FILES],$this->required_files)); 962 unset($this->required_files); 963 } 964 $_SESSION[self::EGW_SESSION_VAR] = array( 965 'session_id' => $this->sessionid, 966 'session_lid' => $login, 967 'session_ip' => $user_ip, 968 'session_logintime' => $now, 969 'session_dla' => $now, 970 'session_action' => $_SERVER['PHP_SELF'], 971 'session_flags' => $session_flags, 972 // we need the install-id to differ between serveral installs shareing one tmp-dir 973 'session_install_id' => $GLOBALS['egw_info']['server']['install_id'] 974 ); 975 } 976 977 /** 978 * name of access-log table 979 */ 980 const ACCESS_LOG_TABLE = 'egw_access_log'; 981 982 /** 983 * Prefix used to log unsucessful login attempts in cache, if DB is unavailable 984 */ 985 const FALSE_IP_CACHE_PREFIX = 'false_ip-'; 986 const FALSE_ID_CACHE_PREFIX = 'false_id-'; 987 988 /** 989 * Write or update (for logout) the access_log 990 * 991 * We do NOT log anonymous sessions to not block website and also to cope with 992 * high rate anon endpoints might be called creating a bottleneck in the egw_access_log table. 993 * 994 * @param string|int $sessionid nummeric or PHP session id or error-message for unsuccessful logins 995 * @param string $login ='' account_lid (evtl. with domain) or '' for setting the logout-time 996 * @param string $user_ip ='' ip to log 997 * @param int $account_id =0 numerical account_id 998 * @return int $sessionid primary key of egw_access_log for login, null otherwise 999 */ 1000 private function log_access($sessionid,$login='',$user_ip='',$account_id=0) 1001 { 1002 // do not log anything for anonymous sessions 1003 if ($this->session_flags === 'A') 1004 { 1005 return; 1006 } 1007 $now = time(); 1008 1009 // if sessionid contains non-ascii chars (only happens for error-messages) 1010 // --> transliterate it to ascii, as session_php only allows ascii chars 1011 if (preg_match('/[^\x20-\x7f]/', $sessionid)) 1012 { 1013 $sessionid = Translation::to_ascii($sessionid); 1014 } 1015 1016 if ($login) 1017 { 1018 $GLOBALS['egw']->db->insert(self::ACCESS_LOG_TABLE,array( 1019 'session_php' => $sessionid, 1020 'loginid' => $login, 1021 'ip' => $user_ip, 1022 'li' => $now, 1023 'account_id'=> $account_id, 1024 'user_agent'=> $_SERVER['HTTP_USER_AGENT'], 1025 'session_dla' => $now, 1026 'session_action' => $this->update_dla(false), // dont update egw_access_log 1027 ),false,__LINE__,__FILE__); 1028 1029 $_SESSION[self::EGW_SESSION_VAR]['session_logged_dla'] = $now; 1030 1031 $ret = $GLOBALS['egw']->db->get_last_insert_id(self::ACCESS_LOG_TABLE,'sessionid'); 1032 1033 // if we can not store failed login attempts in database, store it in cache 1034 if (!$ret && !$account_id) 1035 { 1036 Cache::setInstance(__CLASS__, self::FALSE_IP_CACHE_PREFIX.$user_ip, 1037 1+Cache::getInstance(__CLASS__, self::FALSE_IP_CACHE_PREFIX.$user_ip), 1038 $GLOBALS['egw_info']['server']['block_time'] * 60); 1039 1040 Cache::setInstance(__CLASS__, self::FALSE_ID_CACHE_PREFIX.$login, 1041 1+Cache::getInstance(__CLASS__, self::FALSE_ID_CACHE_PREFIX.$login), 1042 $GLOBALS['egw_info']['server']['block_time'] * 60); 1043 } 1044 } 1045 else 1046 { 1047 if (!is_numeric($sessionid) && $sessionid == $this->sessionid && $this->sessionid_access_log) 1048 { 1049 $sessionid = $this->sessionid_access_log; 1050 } 1051 $GLOBALS['egw']->db->update(self::ACCESS_LOG_TABLE,array( 1052 'lo' => $now 1053 ),is_numeric($sessionid) ? array( 1054 'sessionid' => $sessionid, 1055 ) : array( 1056 'session_php' => $sessionid, 1057 ),__LINE__,__FILE__); 1058 1059 // run maintenance only on logout, to not delay login 1060 if ($GLOBALS['egw_info']['server']['max_access_log_age']) 1061 { 1062 $max_age = $now - $GLOBALS['egw_info']['server']['max_access_log_age'] * 24 * 60 * 60; 1063 1064 $GLOBALS['egw']->db->delete(self::ACCESS_LOG_TABLE,"li < $max_age",__LINE__,__FILE__); 1065 } 1066 } 1067 //error_log(__METHOD__."('$sessionid', '$login', '$user_ip', $account_id) returning ".array2string($ret)); 1068 return $ret; 1069 } 1070 1071 /** 1072 * Protect against brute force attacks, block login if too many unsuccessful login attmepts 1073 * 1074 * @param string $login account_lid (evtl. with domain) 1075 * @param string $ip ip of the user 1076 * @returns bool login blocked? 1077 */ 1078 private function login_blocked($login,$ip) 1079 { 1080 $block_time = time() - $GLOBALS['egw_info']['server']['block_time'] * 60; 1081 1082 $false_id = $false_ip = 0; 1083 foreach($GLOBALS['egw']->db->union(array( 1084 array( 1085 'table' => self::ACCESS_LOG_TABLE, 1086 'cols' => "'false_ip' AS name,COUNT(*) AS num", 1087 'where' => array( 1088 'account_id' => 0, 1089 'ip' => $ip, 1090 "li > $block_time", 1091 ), 1092 ), 1093 array( 1094 'table' => self::ACCESS_LOG_TABLE, 1095 'cols' => "'false_id' AS name,COUNT(*) AS num", 1096 'where' => array( 1097 'account_id' => 0, 1098 'loginid' => $login, 1099 "li > $block_time", 1100 ), 1101 ), 1102 array( 1103 'table' => self::ACCESS_LOG_TABLE, 1104 'cols' => "'false_id' AS name,COUNT(*) AS num", 1105 'where' => array( 1106 'account_id' => 0, 1107 'loginid LIKE '.$GLOBALS['egw']->db->quote($login.'@%'), 1108 "li > $block_time", 1109 ) 1110 ), 1111 ), __LINE__, __FILE__) as $row) 1112 { 1113 ${$row['name']} += $row['num']; 1114 } 1115 1116 // check cache too, in case DB is readonly 1117 $false_ip += Cache::getInstance(__CLASS__, self::FALSE_IP_CACHE_PREFIX.$ip); 1118 $false_id += Cache::getInstance(__CLASS__, self::FALSE_ID_CACHE_PREFIX.$login); 1119 1120 // if IP matches one in the (comma-separated) whitelist 1121 // --> check with whitelists optional number (none means never block) 1122 $matches = null; 1123 if (!empty($GLOBALS['egw_info']['server']['unsuccessful_ip_whitelist']) && 1124 preg_match_all('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?/', 1125 $GLOBALS['egw_info']['server']['unsuccessful_ip_whitelist'], $matches) && 1126 ($key=array_search($ip, $matches[1])) !== false) 1127 { 1128 $blocked = !empty($matches[3][$key]) && $false_ip > $matches[3][$key]; 1129 } 1130 else // else check with general number 1131 { 1132 $blocked = $false_ip > $GLOBALS['egw_info']['server']['num_unsuccessful_ip']; 1133 } 1134 if (!$blocked) 1135 { 1136 $blocked = $false_id > $GLOBALS['egw_info']['server']['num_unsuccessful_id']; 1137 } 1138 //error_log(__METHOD__."('$login', '$ip') false_ip=$false_ip, false_id=$false_id --> blocked=".array2string($blocked)); 1139 1140 if ($blocked && $GLOBALS['egw_info']['server']['admin_mails'] && 1141 $GLOBALS['egw_info']['server']['login_blocked_mail_time'] < time()-5*60) // max. one mail every 5mins 1142 { 1143 try { 1144 $mailer = new Mailer(); 1145 // notify admin(s) via email 1146 $mailer->setFrom('eGroupWare@'.$GLOBALS['egw_info']['server']['mail_suffix']); 1147 $mailer->addHeader('Subject', lang("eGroupWare: login blocked for user '%1', IP %2",$login,$ip)); 1148 $mailer->setBody(lang("Too many unsucessful attempts to login: %1 for the user '%2', %3 for the IP %4",$false_id,$login,$false_ip,$ip)); 1149 foreach(preg_split('/,\s*/',$GLOBALS['egw_info']['server']['admin_mails']) as $mail) 1150 { 1151 $mailer->addAddress($mail); 1152 } 1153 $mailer->send(); 1154 } 1155 catch(\Exception $e) { 1156 // ignore exception, but log it, to block the account and give a correct error-message to user 1157 error_log(__METHOD__."('$login', '$ip') ".$e->getMessage()); 1158 } 1159 // save time of mail, to not send to many mails 1160 $config = new Config('phpgwapi'); 1161 $config->read_repository(); 1162 $config->value('login_blocked_mail_time',time()); 1163 $config->save_repository(); 1164 } 1165 return $blocked; 1166 } 1167 1168 /** 1169 * Basename of scripts for which we create a pseudo session-id based on user-credentials 1170 * 1171 * @var array 1172 */ 1173 static $pseudo_session_scripts = array( 1174 'webdav.php', 'groupdav.php', 'remote.php' 1175 ); 1176 1177 /** 1178 * Get the sessionid from Cookie, Get-Parameter or basic auth 1179 * 1180 * @param boolean $only_basic_auth =false return only a basic auth pseudo sessionid, default no 1181 * @return string|null (pseudo-)session-id use or NULL if no Cookie or Basic-Auth credentials 1182 */ 1183 static function get_sessionid($only_basic_auth=false) 1184 { 1185 // for WebDAV and GroupDAV we use a pseudo sessionid created from md5(user:passwd) 1186 // --> allows this stateless protocolls which use basic auth to use sessions! 1187 if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) && 1188 (in_array(basename($_SERVER['SCRIPT_NAME']), self::$pseudo_session_scripts) || 1189 $_SERVER['SCRIPT_NAME'] === '/Microsoft-Server-ActiveSync')) 1190 { 1191 // we generate a pseudo-sessionid from the basic auth credentials 1192 $sessionid = md5($_SERVER['PHP_AUTH_USER'].':'.$_SERVER['PHP_AUTH_PW'].':'.$_SERVER['HTTP_HOST'].':'. 1193 EGW_SERVER_ROOT.':'.self::getuser_ip().':'.filemtime(EGW_SERVER_ROOT.'/api/setup/setup.inc.php'). 1194 // for ActiveSync we add the DeviceID 1195 (isset($_GET['DeviceId']) && $_SERVER['SCRIPT_NAME'] === '/Microsoft-Server-ActiveSync' ? ':'.$_GET['DeviceId'] : ''). 1196 ':'.$_SERVER['HTTP_USER_AGENT']); 1197 //error_log(__METHOD__."($only_basic_auth) HTTP_HOST=$_SERVER[HTTP_HOST], PHP_AUTH_USER=$_SERVER[PHP_AUTH_USER], DeviceId=$_GET[DeviceId]: sessionid=$sessionid"); 1198 } 1199 // same for digest auth 1200 elseif (isset($_SERVER['PHP_AUTH_DIGEST']) && 1201 in_array(basename($_SERVER['SCRIPT_NAME']), self::$pseudo_session_scripts)) 1202 { 1203 // we generate a pseudo-sessionid from the digest username, realm and nounce 1204 // can't use full $_SERVER['PHP_AUTH_DIGEST'], as it changes (contains eg. the url) 1205 $data = Header\Authenticate::parse_digest($_SERVER['PHP_AUTH_DIGEST']); 1206 $sessionid = md5($data['username'].':'.$data['realm'].':'.$data['nonce'].':'.$_SERVER['HTTP_HOST']. 1207 EGW_SERVER_ROOT.':'.self::getuser_ip().':'.filemtime(EGW_SERVER_ROOT.'/api/setup/setup.inc.php'). 1208 ':'.$_SERVER['HTTP_USER_AGENT']); 1209 } 1210 elseif(!$only_basic_auth && isset($_REQUEST[self::EGW_SESSION_NAME])) 1211 { 1212 $sessionid = $_REQUEST[self::EGW_SESSION_NAME]; 1213 } 1214 elseif(!$only_basic_auth && isset($_COOKIE[self::EGW_SESSION_NAME])) 1215 { 1216 $sessionid = $_COOKIE[self::EGW_SESSION_NAME]; 1217 } 1218 else 1219 { 1220 $sessionid = null; 1221 } 1222 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() _SERVER[REQUEST_URI]='$_SERVER[REQUEST_URI]' returning ".print_r($sessionid,true)); 1223 return $sessionid; 1224 } 1225 1226 /** 1227 * Get request or cookie variable with higher precedence to $_REQUEST then $_COOKIE 1228 * 1229 * In php < 5.3 that's identical to $_REQUEST[$name], but php5.3+ does no longer register cookied in $_REQUEST by default 1230 * 1231 * As a workaround for a bug in Safari Version 3.2.1 (5525.27.1), where cookie first letter get's upcased, we check that too. 1232 * 1233 * @param string $name eg. 'kp3' or domain 1234 * @return mixed null if it's neither set in $_REQUEST or $_COOKIE 1235 */ 1236 static function get_request($name) 1237 { 1238 return isset($_REQUEST[$name]) ? $_REQUEST[$name] : 1239 (isset($_COOKIE[$name]) ? $_COOKIE[$name] : 1240 (isset($_COOKIE[$name=ucfirst($name)]) ? $_COOKIE[$name] : null)); 1241 } 1242 1243 /** 1244 * Check to see if a session is still current and valid 1245 * 1246 * @param string $sessionid session id to be verfied 1247 * @param string $kp3 ?? to be verified 1248 * @return bool is the session valid? 1249 */ 1250 function verify($sessionid=null,$kp3=null) 1251 { 1252 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid','$kp3') ".function_backtrace()); 1253 1254 $fill_egw_info_and_repositories = !$GLOBALS['egw_info']['flags']['restored_from_session']; 1255 1256 if(!$sessionid) 1257 { 1258 $sessionid = self::get_sessionid(); 1259 $kp3 = self::get_request('kp3'); 1260 } 1261 1262 $this->sessionid = $sessionid; 1263 $this->kp3 = $kp3; 1264 1265 1266 if (!$this->sessionid) 1267 { 1268 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') get_sessionid()='".self::get_sessionid()."' No session ID"); 1269 return false; 1270 } 1271 1272 switch (session_status()) 1273 { 1274 case PHP_SESSION_DISABLED: 1275 throw new ErrorException('EGroupware requires the PHP session extension!'); 1276 case PHP_SESSION_NONE: 1277 session_name(self::EGW_SESSION_NAME); 1278 session_id($this->sessionid); 1279 self::cache_control(); 1280 session_start(); 1281 break; 1282 case PHP_SESSION_ACTIVE: 1283 // session already started eg. by managementserver_client 1284 } 1285 1286 // check if we have a eGroupware session --> return false if not (but dont destroy it!) 1287 if (is_null($_SESSION) || !isset($_SESSION[self::EGW_SESSION_VAR])) 1288 { 1289 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') session does NOT exist!"); 1290 return false; 1291 } 1292 $session =& $_SESSION[self::EGW_SESSION_VAR]; 1293 1294 if ($session['session_dla'] <= time() - $GLOBALS['egw_info']['server']['sessions_timeout']) 1295 { 1296 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') session timed out!"); 1297 $this->destroy($sessionid,$kp3); 1298 return false; 1299 } 1300 1301 $this->session_flags = $session['session_flags']; 1302 1303 $this->split_login_domain($session['session_lid'],$this->account_lid,$this->account_domain); 1304 1305 // This is to ensure that we authenticate to the correct domain (might not be default) 1306 if($GLOBALS['egw_info']['user']['domain'] && $this->account_domain != $GLOBALS['egw_info']['user']['domain']) 1307 { 1308 return false; // session not verified, domain changed 1309 } 1310 $GLOBALS['egw_info']['user']['kp3'] = $this->kp3; 1311 1312 // allow xajax / notifications to not update the dla, so sessions can time out again 1313 if (!isset($GLOBALS['egw_info']['flags']['no_dla_update']) || !$GLOBALS['egw_info']['flags']['no_dla_update']) 1314 { 1315 $this->update_dla(true); 1316 } 1317 elseif ($GLOBALS['egw_info']['flags']['currentapp'] == 'notifications') 1318 { 1319 $this->update_notification_heartbeat(); 1320 } 1321 $this->account_id = $GLOBALS['egw']->accounts->name2id($this->account_lid,'account_lid','u'); 1322 if (!$this->account_id) 1323 { 1324 if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) !accounts::name2id('$this->account_lid')"); 1325 return false; 1326 } 1327 1328 $GLOBALS['egw_info']['user']['account_id'] = $this->account_id; 1329 1330 if ($fill_egw_info_and_repositories) 1331 { 1332 $GLOBALS['egw_info']['user'] = $this->read_repositories(); 1333 } 1334 else 1335 { 1336 // restore apps to $GLOBALS['egw_info']['apps'] 1337 $GLOBALS['egw']->applications->read_installed_apps(); 1338 1339 // session only stores app-names, restore apps from egw_info[apps] 1340 if (isset($GLOBALS['egw_info']['user']['apps'][0])) 1341 { 1342 $GLOBALS['egw_info']['user']['apps'] = array_intersect_key($GLOBALS['egw_info']['apps'], array_flip($GLOBALS['egw_info']['user']['apps'])); 1343 } 1344 1345 // set prefs, they are no longer stored in session 1346 $GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository(); 1347 } 1348 1349 if ($GLOBALS['egw']->accounts->is_expired($GLOBALS['egw_info']['user'])) 1350 { 1351 if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) accounts is expired"); 1352 return false; 1353 } 1354 $this->passwd = base64_decode(Cache::getSession('phpgwapi', 'password')); 1355 if ($fill_egw_info_and_repositories) 1356 { 1357 $GLOBALS['egw_info']['user']['session_ip'] = $session['session_ip']; 1358 $GLOBALS['egw_info']['user']['passwd'] = $this->passwd; 1359 } 1360 if ($this->account_domain != $GLOBALS['egw_info']['user']['domain']) 1361 { 1362 if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) wrong domain"); 1363 return false; 1364 } 1365 1366 if ($GLOBALS['egw_info']['server']['sessions_checkip']) 1367 { 1368 if (strtoupper(substr(PHP_OS,0,3)) != 'WIN' && (!$GLOBALS['egw_info']['user']['session_ip'] || 1369 $GLOBALS['egw_info']['user']['session_ip'] != $this->getuser_ip())) 1370 { 1371 if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) wrong IP"); 1372 return false; 1373 } 1374 } 1375 1376 if ($fill_egw_info_and_repositories) 1377 { 1378 $GLOBALS['egw']->acl->__construct($this->account_id); 1379 $GLOBALS['egw']->preferences->__construct($this->account_id); 1380 $GLOBALS['egw']->applications->__construct($this->account_id); 1381 } 1382 if (!$this->account_lid) 1383 { 1384 if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) !account_lid"); 1385 return false; 1386 } 1387 1388 // query accesslog-id, if not set in session (session is made persistent after login!) 1389 if (!$this->sessionid_access_log && $this->session_flags != 'A') 1390 { 1391 $this->sessionid_access_log = $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE,'sessionid',array( 1392 'session_php' => $this->sessionid, 1393 ),__LINE__,__FILE__)->fetchColumn(); 1394 //error_log(__METHOD__."() sessionid=$this->sessionid --> sessionid_access_log=$this->sessionid_access_log"); 1395 } 1396 1397 // check if we use cookies for the session, but no cookie set 1398 // happens eg. in sitemgr (when redirecting to a different domain) or with new java notification app 1399 if ($GLOBALS['egw_info']['server']['usecookies'] && isset($_REQUEST[self::EGW_SESSION_NAME]) && 1400 $_REQUEST[self::EGW_SESSION_NAME] === $this->sessionid && 1401 (!isset($_COOKIE[self::EGW_SESSION_NAME]) || $_COOKIE[self::EGW_SESSION_NAME] !== $_REQUEST[self::EGW_SESSION_NAME])) 1402 { 1403 if (self::ERROR_LOG_DEBUG) error_log("--> Session::verify($sessionid) SUCCESS, but NO required cookies set --> setting them now"); 1404 self::egw_setcookie(self::EGW_SESSION_NAME,$this->sessionid); 1405 self::egw_setcookie('kp3',$this->kp3); 1406 self::egw_setcookie('domain',$this->account_domain); 1407 } 1408 1409 if (self::ERROR_LOG_DEBUG) error_log("--> Session::verify($sessionid) SUCCESS"); 1410 1411 return true; 1412 } 1413 1414 /** 1415 * Terminate a session 1416 * 1417 * @param int|string $sessionid nummeric or php session id of session to be terminated 1418 * @param string $kp3 1419 * @return boolean true on success, false on error 1420 */ 1421 function destroy($sessionid, $kp3='') 1422 { 1423 if (!$sessionid && $kp3) 1424 { 1425 return false; 1426 } 1427 $this->log_access($sessionid); // log logout-time 1428 1429 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($sessionid,$kp3)"); 1430 1431 if (is_numeric($sessionid)) // do we have a access-log-id --> get PHP session id 1432 { 1433 $sessionid = $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE,'session_php',array( 1434 'sessionid' => $sessionid, 1435 ),__LINE__,__FILE__)->fetchColumn(); 1436 } 1437 1438 Hooks::process(array( 1439 'location' => 'session_destroyed', 1440 'sessionid' => $sessionid, 1441 ),'',true); // true = run hooks from all apps, not just the ones the current user has perms to run 1442 1443 // Only do the following, if where working with the current user 1444 if (!$GLOBALS['egw_info']['user']['sessionid'] || $sessionid == $GLOBALS['egw_info']['user']['sessionid']) 1445 { 1446 // eg. SAML logout will fail, if there is no more session --> remove everything else 1447 $auth = new Auth(); 1448 if (($needed = $auth->needSession()) && array_intersect($needed, array_keys($_SESSION))) 1449 { 1450 $_SESSION = array_intersect_key($_SESSION, array_flip($needed)); 1451 Auth::backend($auth->backendType()); // backend is stored in session 1452 return true; 1453 } 1454 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__." ********* about to call session_destroy!"); 1455 session_unset(); 1456 @session_destroy(); 1457 // we need to (re-)load the eGW session-handler, as session_destroy unloads custom session-handlers 1458 if (function_exists('init_session_handler')) 1459 { 1460 init_session_handler(); 1461 } 1462 1463 if ($GLOBALS['egw_info']['server']['usecookies']) 1464 { 1465 self::egw_setcookie(session_name()); 1466 } 1467 } 1468 else 1469 { 1470 $this->commit_session(); // close our own session 1471 1472 session_id($sessionid); 1473 if (session_start()) 1474 { 1475 session_destroy(); 1476 } 1477 } 1478 return true; 1479 } 1480 1481 /** 1482 * Generate a url which supports url or cookies based sessions 1483 * 1484 * Please note, the values of the query get url encoded! 1485 * 1486 * @param string $url a url relative to the egroupware install root, it can contain a query too 1487 * @param array|string $extravars query string arguements as string or array (prefered) 1488 * if string is used ambersands in vars have to be already urlencoded as '%26', function ensures they get NOT double encoded 1489 * @return string generated url 1490 */ 1491 public static function link($url, $extravars = '') 1492 { 1493 //error_log(_METHOD__."(url='$url',extravars='".array2string($extravars)."')"); 1494 1495 if ($url[0] != '/') 1496 { 1497 $app = $GLOBALS['egw_info']['flags']['currentapp']; 1498 if ($app != 'login' && $app != 'logout') 1499 { 1500 $url = $app.'/'.$url; 1501 } 1502 } 1503 1504 // append the url to the webserver url, but avoid more then one slash between the parts of the url 1505 $webserver_url = $GLOBALS['egw_info']['server']['webserver_url']; 1506 // patch inspired by vladimir kolobkov -> we should not try to match the webserver url against the url without '/' as delimiter, 1507 // as $webserver_url may be part of $url (as /egw is part of phpgwapi/js/egw_instant_load.html) 1508 if (($url[0] != '/' || $webserver_url != '/') && (!$webserver_url || strpos($url, $webserver_url.'/') === false)) 1509 { 1510 if($url[0] != '/' && substr($webserver_url,-1) != '/') 1511 { 1512 $url = $webserver_url .'/'. $url; 1513 } 1514 else 1515 { 1516 $url = $webserver_url . $url; 1517 } 1518 } 1519 1520 if(isset($GLOBALS['egw_info']['server']['enforce_ssl']) && $GLOBALS['egw_info']['server']['enforce_ssl']) 1521 { 1522 if(substr($url ,0,4) != 'http') 1523 { 1524 $url = 'https://'.$_SERVER['HTTP_HOST'].$url; 1525 } 1526 else 1527 { 1528 $url = str_replace ( 'http:', 'https:', $url); 1529 } 1530 } 1531 $vars = array(); 1532 // add session params if not using cookies 1533 if (!$GLOBALS['egw_info']['server']['usecookies']) 1534 { 1535 $vars[self::EGW_SESSION_NAME] = $GLOBALS['egw']->session->sessionid; 1536 $vars['kp3'] = $GLOBALS['egw']->session->kp3; 1537 $vars['domain'] = $GLOBALS['egw']->session->account_domain; 1538 } 1539 1540 // check if the url already contains a query and ensure that vars is an array and all strings are in extravars 1541 list($ret_url,$othervars) = explode('?', $url, 2); 1542 if ($extravars && is_array($extravars)) 1543 { 1544 $vars += $extravars; 1545 $extravars = $othervars; 1546 } 1547 else 1548 { 1549 if ($othervars) $extravars .= ($extravars?'&':'').$othervars; 1550 } 1551 1552 // parse extravars string into the vars array 1553 if ($extravars) 1554 { 1555 foreach(explode('&',$extravars) as $expr) 1556 { 1557 list($var,$val) = explode('=', $expr,2); 1558 if (strpos($val,'%26') != false) $val = str_replace('%26','&',$val); // make sure to not double encode & 1559 if (substr($var,-2) == '[]') 1560 { 1561 $vars[substr($var,0,-2)][] = $val; 1562 } 1563 else 1564 { 1565 $vars[$var] = $val; 1566 } 1567 } 1568 } 1569 1570 // if there are vars, we add them urlencoded to the url 1571 if (count($vars)) 1572 { 1573 $query = array(); 1574 foreach($vars as $key => $value) 1575 { 1576 if (is_array($value)) 1577 { 1578 foreach($value as $val) 1579 { 1580 $query[] = $key.'[]='.urlencode($val); 1581 } 1582 } 1583 else 1584 { 1585 $query[] = $key.'='.urlencode($value); 1586 } 1587 } 1588 $ret_url .= '?' . implode('&',$query); 1589 } 1590 return $ret_url; 1591 } 1592 1593 /** 1594 * Regexp to validate IPv4 and IPv6 1595 */ 1596 const IP_REGEXP = '/^(?>(?>([a-f0-9]{1,4})(?>:(?1)){7}|(?!(?:.*[a-f0-9](?>:|$)){8,})((?1)(?>:(?1)){0,6})?::(?2)?)|(?>(?>(?1)(?>:(?1)){5}:|(?!(?:.*[a-f0-9]:){6,})(?3)?::(?>((?1)(?>:(?1)){0,4}):)?)?(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?>\.(?4)){3}))$/iD'; 1597 1598 /** 1599 * Get the ip address of current users 1600 * 1601 * We remove further private IPs (from proxys) as they invalidate user 1602 * sessions, when they change because of multiple proxys. 1603 * 1604 * @return string ip address 1605 */ 1606 public static function getuser_ip() 1607 { 1608 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) 1609 { 1610 $forwarded_for = preg_replace('/, *10\..*$/', '', $_SERVER['HTTP_X_FORWARDED_FOR']); 1611 if (preg_match(self::IP_REGEXP, $forwarded_for)) 1612 { 1613 return $forwarded_for; 1614 } 1615 } 1616 return $_SERVER['REMOTE_ADDR']; 1617 } 1618 1619 /** 1620 * domain for cookies 1621 * 1622 * @var string 1623 */ 1624 private static $cookie_domain = ''; 1625 1626 /** 1627 * path for cookies 1628 * 1629 * @var string 1630 */ 1631 private static $cookie_path = '/'; 1632 1633 /** 1634 * secure cookie / send via https only 1635 * 1636 * @var bool 1637 */ 1638 private static $cookie_secure = false; 1639 1640 /** 1641 * iOS web-apps will loose cookie if set with a livetime of 0 / session-cookie 1642 * 1643 * Therefore we set a fixed lifetime of 24h from session-start instead. 1644 * Server-side session will timeout earliert anyway, if there's no activity. 1645 */ 1646 const IOS_SESSION_COOKIE_LIFETIME = 86400; 1647 1648 /** 1649 * Set a cookie with eGW's cookie-domain and -path settings 1650 * 1651 * @param string $cookiename name of cookie to be set 1652 * @param string $cookievalue ='' value to be used, if unset cookie is cleared (optional) 1653 * @param int $cookietime =0 when cookie should expire, 0 for session only (optional) 1654 * @param string $cookiepath =null optional path (eg. '/') if the eGW install-dir should not be used 1655 */ 1656 public static function egw_setcookie($cookiename,$cookievalue='',$cookietime=0,$cookiepath=null) 1657 { 1658 if (empty(self::$cookie_domain) || empty(self::$cookie_path)) 1659 { 1660 self::set_cookiedomain(); 1661 } 1662 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($cookiename,$cookievalue,$cookietime,$cookiepath,".self::$cookie_domain.")"); 1663 1664 // if we are installed in iOS as web-app, we must not set a cookietime==0 (session-cookie), 1665 // as every change between apps will cause the cookie to get lost 1666 static $is_iOS = null; 1667 if (!$cookietime && !isset($is_iOS)) $is_iOS = (bool)preg_match('/^(iPhone|iPad|iPod)/i', Header\UserAgent::mobile()); 1668 1669 if(!headers_sent()) // gives only a warning, but can not send the cookie anyway 1670 { 1671 $options = [ 1672 'expires' => !$cookietime && $is_iOS ? time()+self::IOS_SESSION_COOKIE_LIFETIME : $cookietime, 1673 'path' => is_null($cookiepath) ? self::$cookie_path : $cookiepath, 1674 'domain' => self::$cookie_domain, 1675 // if called via HTTPS, only send cookie for https 1676 'secure' => empty($GLOBALS['egw_info']['server']['insecure_cookies']) && Header\Http::schema() === 'https', 1677 'httponly' => true, // only allow cookie access via HTTP, not client-side via JavaScript 1678 ]; 1679 // admin specified to send SameSite cookie attribute AND we use PHP 7.3+ 1680 if (!empty($GLOBALS['egw_info']['server']['cookie_samesite_attribute']) && 1681 in_array($GLOBALS['egw_info']['server']['cookie_samesite_attribute'], ['Lax', 'Strict', 'None'])) 1682 { 1683 $options['samesite'] = $GLOBALS['egw_info']['server']['cookie_samesite_attribute']; 1684 } 1685 if ((float)PHP_VERSION >= 7.3) 1686 { 1687 setcookie($cookiename, $cookievalue, $options); 1688 } 1689 else 1690 { 1691 setcookie($cookiename, $cookievalue, 1692 $options['expires'], $options['path'], $options['domain'], $options['secure'], $options['httponly']); 1693 } 1694 } 1695 } 1696 1697 /** 1698 * Get cookie-domain and other cookie parameters used by EGroupware 1699 * 1700 * @param string& $path =null on return cookie path, by default "/", but can be configured 1701 * @param bool& $secure =null on return 1702 * @return string domain-name used (either configured one or current one with a leading dot eg. ".example.org") 1703 */ 1704 public function getCookieDomain(&$path=null, &$secure=null) 1705 { 1706 if (empty(self::$cookie_domain) || empty(self::$cookie_path)) 1707 { 1708 self::set_cookiedomain(); 1709 } 1710 $path = self::$cookie_path; 1711 $secure = self::$cookie_secure; 1712 1713 return self::$cookie_domain; 1714 } 1715 1716 /** 1717 * Set the domain and path used for cookies 1718 */ 1719 private static function set_cookiedomain() 1720 { 1721 if (PHP_SAPI === "cli") return; // gives warnings and has no benefit 1722 1723 if ($GLOBALS['egw_info']['server']['cookiedomain']) 1724 { 1725 // Admin set domain, eg. .domain.com to allow egw.domain.com and www.domain.com 1726 self::$cookie_domain = $GLOBALS['egw_info']['server']['cookiedomain']; 1727 } 1728 else 1729 { 1730 // Use HTTP_X_FORWARDED_HOST if set, which is the case behind a none-transparent proxy 1731 self::$cookie_domain = Header\Http::host(); 1732 } 1733 // remove port from HTTP_HOST 1734 $arr = null; 1735 if (preg_match("/^(.*):(.*)$/",self::$cookie_domain,$arr)) 1736 { 1737 self::$cookie_domain = $arr[1]; 1738 } 1739 if (count(explode('.',self::$cookie_domain)) <= 1) 1740 { 1741 // setcookie dont likes domains without dots, leaving it empty, gets setcookie to fill the domain in 1742 self::$cookie_domain = ''; 1743 } 1744 if (!$GLOBALS['egw_info']['server']['cookiepath'] || 1745 !(self::$cookie_path = parse_url($GLOBALS['egw_info']['server']['webserver_url'],PHP_URL_PATH))) 1746 { 1747 self::$cookie_path = '/'; 1748 } 1749 1750 // if called via HTTPS, only send cookie for https and only allow cookie access via HTTP (true) 1751 self::$cookie_secure = empty($GLOBALS['egw_info']['server']['insecure_cookies']) && Header\Http::schema() === 'https'; 1752 1753 session_set_cookie_params(0, self::$cookie_path, self::$cookie_domain, self::$cookie_secure, true); 1754 } 1755 1756 /** 1757 * Search the instance matching the request 1758 * 1759 * @param string $login on login $_POST['login'], $_SERVER['PHP_AUTH_USER'] or $_SERVER['REMOTE_USER'] 1760 * @param string $domain_requested usually self::get_request('domain') 1761 * @param string &$default_domain usually $default_domain get's set eg. by sitemgr 1762 * @param string|array $server_names usually array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) 1763 * @param array $domains =null defaults to $GLOBALS['egw_domain'] from the header 1764 * @return string $GLOBALS['egw_info']['user']['domain'] set with the domain/instance to use 1765 */ 1766 public static function search_instance($login,$domain_requested,&$default_domain,$server_names,array $domains=null) 1767 { 1768 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$login','$domain_requested',".array2string($default_domain).".".array2string($server_names).".".array2string($domains).")"); 1769 1770 if (is_null($domains)) $domains = $GLOBALS['egw_domain']; 1771 1772 if (!isset($default_domain) || !isset($domains[$default_domain])) // allow to overwrite the default domain 1773 { 1774 foreach((array)$server_names as $server_name) 1775 { 1776 list($server_name) = explode(':', $server_name); // remove port from HTTP_HOST 1777 if(isset($domains[$server_name])) 1778 { 1779 $default_domain = $server_name; 1780 break; 1781 } 1782 else 1783 { 1784 $parts = explode('.', $server_name); 1785 array_shift($parts); 1786 $domain_part = implode('.', $parts); 1787 if(isset($domains[$domain_part])) 1788 { 1789 $default_domain = $domain_part; 1790 break; 1791 } 1792 else 1793 { 1794 reset($domains); 1795 $default_domain = key($domains); 1796 } 1797 unset($domain_part); 1798 } 1799 } 1800 } 1801 if (isset($login)) // on login 1802 { 1803 if (strpos($login,'@') === false || count($domains) == 1) 1804 { 1805 $login .= '@' . (isset($_POST['logindomain']) ? $_POST['logindomain'] : $default_domain); 1806 } 1807 $parts = explode('@',$login); 1808 $domain = array_pop($parts); 1809 $GLOBALS['login'] = $login; 1810 } 1811 else // on "normal" pageview 1812 { 1813 $domain = $domain_requested; 1814 } 1815 if (!isset($domains[$domain])) 1816 { 1817 $domain = $default_domain; 1818 } 1819 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() default_domain=".array2string($default_domain).', login='.array2string($login)." returning ".array2string($domain)); 1820 1821 return $domain; 1822 } 1823 1824 /** 1825 * Set action logged in access-log 1826 * 1827 * Non-ascii chars in $action get transliterate to ascii, as our session_action column allows only ascii. 1828 * 1829 * @param string $action 1830 */ 1831 public function set_action($action) 1832 { 1833 if (preg_match('/[^\x20-\x7f]/', $action)) 1834 { 1835 $action = Translation::to_ascii($action); 1836 } 1837 $this->action = $action; 1838 } 1839 1840 /** 1841 * Ignore dla logging for a maximum of 900s = 15min 1842 */ 1843 const MAX_IGNORE_DLA_LOG = 900; 1844 1845 /** 1846 * Update session_action and session_dla (session last used time) 1847 * 1848 * @param boolean $update_access_log =false false: dont update egw_access_log table, but set $this->action 1849 * @return string action as written to egw_access_log.session_action 1850 */ 1851 private function update_dla($update_access_log=false) 1852 { 1853 // This way XML-RPC users aren't always listed as xmlrpc.php 1854 if (isset($_GET['menuaction'])) 1855 { 1856 list(, $action) = explode('.ajax_exec.template.', $_GET['menuaction']); 1857 1858 if (empty($action)) $action = $_GET['menuaction']; 1859 } 1860 else 1861 { 1862 $action = $_SERVER['PHP_SELF']; 1863 // remove EGroupware path, if not installed in webroot 1864 $egw_path = $GLOBALS['egw_info']['server']['webserver_url']; 1865 if ($egw_path[0] != '/') $egw_path = parse_url($egw_path,PHP_URL_PATH); 1866 if ($action == '/Microsoft-Server-ActiveSync') 1867 { 1868 $action .= '?Cmd='.$_GET['Cmd'].'&DeviceId='.$_GET['DeviceId']; 1869 } 1870 elseif ($egw_path) 1871 { 1872 list(,$action) = explode($egw_path,$action,2); 1873 } 1874 } 1875 $this->set_action($action); 1876 1877 // update dla in access-log table, if we have an access-log row (non-anonymous session) 1878 if ($this->sessionid_access_log && $update_access_log && 1879 // ignore updates (session creation is written) of *dav, avatar and thumbnail, due to possible high volume of updates 1880 (!preg_match('#^(/webdav|/groupdav|/api/avatar|/api/thumbnail)\.php#', $this->action) || 1881 (time() - $_SESSION[self::EGW_SESSION_VAR]['session_logged_dla']) > self::MAX_IGNORE_DLA_LOG) && 1882 is_object($GLOBALS['egw']->db)) 1883 { 1884 $_SESSION[self::EGW_SESSION_VAR]['session_logged_dla'] = time(); 1885 1886 $GLOBALS['egw']->db->update(self::ACCESS_LOG_TABLE,array( 1887 'session_dla' => time(), 1888 'session_action' => $this->action, 1889 ) + ($this->action === '/logout.php' ? array() : array( 1890 'lo' => null, // just in case it was (automatic) timed out before 1891 )),array( 1892 'sessionid' => $this->sessionid_access_log, 1893 ),__LINE__,__FILE__); 1894 } 1895 1896 $_SESSION[self::EGW_SESSION_VAR]['session_dla'] = time(); 1897 $_SESSION[self::EGW_SESSION_VAR]['session_action'] = $this->action; 1898 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__.'() _SESSION['.self::EGW_SESSION_VAR.']='.array2string($_SESSION[self::EGW_SESSION_VAR])); 1899 1900 return $this->action; 1901 } 1902 1903 /** 1904 * Update notification_heartbeat time of session 1905 */ 1906 private function update_notification_heartbeat() 1907 { 1908 // update dla in access-log table, if we have an access-log row (non-anonymous session) 1909 if ($this->sessionid_access_log) 1910 { 1911 $GLOBALS['egw']->db->update(self::ACCESS_LOG_TABLE,array( 1912 'notification_heartbeat' => time(), 1913 ),array( 1914 'sessionid' => $this->sessionid_access_log, 1915 'lo IS NULL', 1916 ),__LINE__,__FILE__); 1917 } 1918 } 1919 1920 /** 1921 * Read the diverse repositories / init classes with data from the just loged in user 1922 * 1923 * @return array used to assign to $GLOBALS['egw_info']['user'] 1924 */ 1925 public function read_repositories() 1926 { 1927 $GLOBALS['egw']->acl->__construct($this->account_id); 1928 $GLOBALS['egw']->preferences->__construct($this->account_id); 1929 $GLOBALS['egw']->applications->__construct($this->account_id); 1930 1931 $user = $GLOBALS['egw']->accounts->read($this->account_id); 1932 // set homedirectory from auth_ldap or auth_ads, to be able to use it in vfs 1933 if (!isset($user['homedirectory'])) 1934 { 1935 // authentication happens in login.php, which does NOT yet create egw-object in session 1936 // --> need to store homedirectory in session 1937 if(isset($GLOBALS['auto_create_acct']['homedirectory'])) 1938 { 1939 Cache::setSession(__CLASS__, 'homedirectory', 1940 $user['homedirectory'] = $GLOBALS['auto_create_acct']['homedirectory']); 1941 } 1942 else 1943 { 1944 $user['homedirectory'] = Cache::getSession(__CLASS__, 'homedirectory'); 1945 } 1946 } 1947 $user['preferences'] = $GLOBALS['egw']->preferences->read_repository(); 1948 if (is_object($GLOBALS['egw']->datetime)) 1949 { 1950 $GLOBALS['egw']->datetime->__construct(); // to set tz_offset from the now read prefs 1951 } 1952 $user['apps'] = $GLOBALS['egw']->applications->read_repository(); 1953 $user['domain'] = $this->account_domain; 1954 $user['sessionid'] = $this->sessionid; 1955 $user['kp3'] = $this->kp3; 1956 $user['session_ip'] = $this->getuser_ip(); 1957 $user['session_lid'] = $this->account_lid.'@'.$this->account_domain; 1958 $user['account_id'] = $this->account_id; 1959 $user['account_lid'] = $this->account_lid; 1960 $user['userid'] = $this->account_lid; 1961 $user['passwd'] = $this->passwd; 1962 1963 return $user; 1964 } 1965 1966 /** 1967 * Splits a login-name into account_lid and eGW-domain/-instance 1968 * 1969 * @param string $login login-name (ie. user@default) 1970 * @param string &$account_lid returned account_lid (ie. user) 1971 * @param string &$domain returned domain (ie. domain) 1972 */ 1973 private function split_login_domain($login,&$account_lid,&$domain) 1974 { 1975 $parts = explode('@',$login); 1976 1977 //conference - for strings like vinicius@thyamad.com@default , 1978 //allows that user have a login that is his e-mail. (viniciuscb) 1979 if (count($parts) > 1) 1980 { 1981 $probable_domain = array_pop($parts); 1982 //Last part of login string, when separated by @, is a domain name 1983 if (in_array($probable_domain,$this->egw_domains)) 1984 { 1985 $got_login = true; 1986 $domain = $probable_domain; 1987 $account_lid = implode('@',$parts); 1988 } 1989 } 1990 1991 if (!$got_login) 1992 { 1993 $domain = $GLOBALS['egw_info']['server']['default_domain']; 1994 $account_lid = $login; 1995 } 1996 } 1997 1998 /** 1999 * Create a hash from user and pw 2000 * 2001 * Can be used to check setup config user/password inside egroupware: 2002 * 2003 * if (Api\Session::user_pw_hash($user,$pw) === $GLOBALS['egw_info']['server']['config_hash']) 2004 * 2005 * @param string $user username 2006 * @param string $password password or md5 hash of password if $allow_password_md5 2007 * @param boolean $allow_password_md5 =false can password alread be an md5 hash 2008 * @return string 2009 */ 2010 static function user_pw_hash($user,$password,$allow_password_md5=false) 2011 { 2012 $password_md5 = $allow_password_md5 && preg_match('/^[a-f0-9]{32}$/',$password) ? $password : md5($password); 2013 2014 $hash = sha1(strtolower($user).$password_md5); 2015 2016 return $hash; 2017 } 2018 2019 /** 2020 * Initialise the used session handler 2021 * 2022 * @return boolean true if we have a session, false otherwise 2023 * @throws \ErrorException if there is no PHP session support 2024 */ 2025 public static function init_handler() 2026 { 2027 switch(session_status()) 2028 { 2029 case PHP_SESSION_DISABLED: 2030 throw new \ErrorException('EGroupware requires PHP session extension!'); 2031 case PHP_SESSION_NONE: 2032 if (headers_sent()) return false; // only gives warnings 2033 ini_set('session.use_cookies',0); // disable the automatic use of cookies, as it uses the path / by default 2034 session_name(self::EGW_SESSION_NAME); 2035 if (($sessionid = self::get_sessionid())) 2036 { 2037 session_id($sessionid); 2038 self::cache_control(); 2039 $ok = session_start(); 2040 self::decrypt(); 2041 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() sessionid=$sessionid, _SESSION[".self::EGW_SESSION_VAR.']='.array2string($_SESSION[self::EGW_SESSION_VAR])); 2042 return $ok; 2043 } 2044 break; 2045 case PHP_SESSION_ACTIVE: 2046 return true; // session created by MServer 2047 } 2048 if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() no active session!"); 2049 2050 return false; 2051 } 2052 2053 /** 2054 * Controling caching and expires header 2055 * 2056 * Headers are send based on given parameters or $GLOBALS['egw_info']['flags']['nocachecontrol']: 2057 * - not set of false --> no caching (default) 2058 * - true --> private caching by browser (no expires header) 2059 * - "public" or integer --> public caching with given cache_expire in minutes or php.ini default session_cache_expire 2060 * 2061 * @param int $expire =null expiration time in seconds, default $GLOBALS['egw_info']['flags']['nocachecontrol'] or php.ini session.cache_expire 2062 * @param int $private =null allows to set private caching with given expiration time, by setting it to true 2063 */ 2064 public static function cache_control($expire=null, $private=null) 2065 { 2066 if (is_null($expire) && isset($GLOBALS['egw_info']['flags']['nocachecontrol']) && is_int($GLOBALS['egw_info']['flags']['nocachecontrol'])) 2067 { 2068 $expire = $GLOBALS['egw_info']['flags']['nocachecontrol']; 2069 } 2070 // session not yet started: use PHP session_cache_limiter() and session_cache_expires() functions 2071 if (!isset($_SESSION)) 2072 { 2073 // controling caching and expires header 2074 if(!isset($expire) && (!isset($GLOBALS['egw_info']['flags']['nocachecontrol']) || 2075 !$GLOBALS['egw_info']['flags']['nocachecontrol'])) 2076 { 2077 session_cache_limiter('nocache'); 2078 } 2079 elseif (isset($expire) || $GLOBALS['egw_info']['flags']['nocachecontrol'] === 'public' || is_int($GLOBALS['egw_info']['flags']['nocachecontrol'])) 2080 { 2081 // allow public caching: proxys, cdns, ... 2082 if (isset($expire)) 2083 { 2084 session_cache_expire((int)ceil($expire/60)); // in minutes 2085 } 2086 session_cache_limiter($private ? 'private' : 'public'); 2087 } 2088 else 2089 { 2090 // allow caching by browser 2091 session_cache_limiter('private_no_expire'); 2092 } 2093 } 2094 // session already started 2095 if (isset($_SESSION)) 2096 { 2097 if ($expire && (session_cache_limiter() !== ($expire===true?'private_no_expire':'public') || 2098 is_int($expire) && $expire/60 !== session_cache_expire())) 2099 { 2100 $file = $line = null; 2101 if (headers_sent($file, $line)) 2102 { 2103 error_log(__METHOD__."($expire) called, but header already sent in $file: $line"); 2104 return; 2105 } 2106 if($expire === true) // same behavior as session_cache_limiter('private_no_expire') 2107 { 2108 header('Cache-Control: private, max-age='.(60*session_cache_expire())); 2109 header_remove('Expires'); 2110 } 2111 elseif ($private) 2112 { 2113 header('Cache-Control: private, max-age='.$expire); 2114 header('Expires: ' . gmdate('D, d M Y H:i:s', time()+$expire) . ' GMT'); 2115 } 2116 else 2117 { 2118 header('Cache-Control: public, max-age='.$expire); 2119 header('Expires: ' . gmdate('D, d M Y H:i:s', time()+$expire) . ' GMT'); 2120 } 2121 // remove Pragma header, might be set by old header 2122 if (function_exists('header_remove')) // PHP 5.3+ 2123 { 2124 header_remove('Pragma'); 2125 } 2126 else 2127 { 2128 header('Pragma:'); 2129 } 2130 } 2131 } 2132 } 2133 2134 /** 2135 * Get a session list (of the current instance) 2136 * 2137 * @param int $start 2138 * @param string $sort ='DESC' ASC or DESC 2139 * @param string $order ='session_dla' session_lid, session_id, session_started, session_logintime, session_action, or (default) session_dla 2140 * @param boolean $all_no_sort =False skip sorting and limiting to maxmatchs if set to true 2141 * @param array $filter =array() extra filter for sessions 2142 * @return array with sessions (values for keys as in $sort) 2143 */ 2144 public static function session_list($start,$sort='DESC',$order='session_dla',$all_no_sort=False,array $filter=array()) 2145 { 2146 $sessions = array(); 2147 if (!preg_match('/^[a-z0-9_ ,]+$/i',$order_by=$order.' '.$sort) || $order_by == ' ') 2148 { 2149 $order_by = 'session_dla DESC'; 2150 } 2151 $filter['lo'] = null; 2152 $filter[] = 'account_id>0'; 2153 $filter[] = 'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']); 2154 $filter[] = '(notification_heartbeat IS NULL OR notification_heartbeat > '.self::heartbeat_limit().')'; 2155 foreach($GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE, '*', $filter, __LINE__, __FILE__, 2156 $all_no_sort ? false : $start, 'ORDER BY '.$order_by) as $row) 2157 { 2158 $sessions[$row['sessionid']] = $row; 2159 } 2160 return $sessions; 2161 } 2162 2163 /** 2164 * Query number of sessions (not more then once every N secs) 2165 * 2166 * @param array $filter =array() extra filter for sessions 2167 * @return int number of active sessions 2168 */ 2169 public static function session_count(array $filter=array()) 2170 { 2171 $filter['lo'] = null; 2172 $filter[] = 'account_id>0'; 2173 $filter[] = 'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']); 2174 $filter[] = '(notification_heartbeat IS NULL OR notification_heartbeat > '.self::heartbeat_limit().')'; 2175 return $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE, 'COUNT(*)', $filter, __LINE__, __FILE__)->fetchColumn(); 2176 } 2177 2178 /** 2179 * Get limit / latest time of heartbeat for session to be active 2180 * 2181 * @return int TS in server-time 2182 */ 2183 public static function heartbeat_limit() 2184 { 2185 static $limit=null; 2186 2187 if (is_null($limit)) 2188 { 2189 $config = Config::read('notifications'); 2190 if (!($popup_poll_interval = $config['popup_poll_interval'])) 2191 { 2192 $popup_poll_interval = 60; 2193 } 2194 $limit = (int)(time() - $popup_poll_interval-10); // 10s grace periode 2195 } 2196 return $limit; 2197 } 2198 2199 /** 2200 * Check if given user can be reached via notifications 2201 * 2202 * Checks if notifications callback checked in not more then heartbeat_limit() seconds ago 2203 * 2204 * @param int $account_id 2205 * @param int number of active sessions of given user with notifications running 2206 */ 2207 public static function notifications_active($account_id) 2208 { 2209 return $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE, 'COUNT(*)', array( 2210 'lo' => null, 2211 'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']), 2212 'account_id' => $account_id, 2213 'notification_heartbeat > '.self::heartbeat_limit(), 2214 ), __LINE__, __FILE__)->fetchColumn(); 2215 } 2216} 2217