Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

354 lignes
9.9 KiB

  1. <?php
  2. /*
  3. * ejabberd extauth script for the integration with friendica
  4. *
  5. * Originally written for joomla by Dalibor Karlovic <dado@krizevci.info>
  6. * modified for Friendica by Michael Vogel <icarus@dabo.de>
  7. * published under GPL
  8. *
  9. * Latest version of the original script for joomla is available at:
  10. * http://87.230.15.86/~dado/ejabberd/joomla-login
  11. *
  12. * Installation:
  13. *
  14. * - Change it's owner to whichever user is running the server, ie. ejabberd
  15. * $ chown ejabberd:ejabberd /path/to/friendica/bin/auth_ejabberd.php
  16. *
  17. * - Change the access mode so it is readable only to the user ejabberd and has exec
  18. * $ chmod 700 /path/to/friendica/bin/auth_ejabberd.php
  19. *
  20. * - Edit your ejabberd.cfg file, comment out your auth_method and add:
  21. * {auth_method, external}.
  22. * {extauth_program, "/path/to/friendica/bin/auth_ejabberd.php"}.
  23. *
  24. * - Restart your ejabberd service, you should be able to login with your friendica auth info
  25. *
  26. * Other hints:
  27. * - if your users have a space or a @ in their nickname, they'll run into trouble
  28. * registering with any client so they should be instructed to replace these chars
  29. * " " (space) is replaced with "%20"
  30. * "@" is replaced with "(a)"
  31. *
  32. */
  33. namespace Friendica\Util;
  34. use Friendica\Core\Config;
  35. use Friendica\Database\DBA;
  36. use Friendica\DI;
  37. use Friendica\Model\User;
  38. class ExAuth
  39. {
  40. private $bDebug;
  41. private $host;
  42. /**
  43. * @brief Create the class
  44. *
  45. */
  46. public function __construct()
  47. {
  48. $this->bDebug = (int) Config::get('jabber', 'debug');
  49. openlog('auth_ejabberd', LOG_PID, LOG_USER);
  50. $this->writeLog(LOG_NOTICE, 'start');
  51. }
  52. /**
  53. * @brief Standard input reading function, executes the auth with the provided
  54. * parameters
  55. *
  56. * @return null
  57. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  58. */
  59. public function readStdin()
  60. {
  61. while (!feof(STDIN)) {
  62. // Quit if the database connection went down
  63. if (!DBA::connected()) {
  64. $this->writeLog(LOG_ERR, 'the database connection went down');
  65. return;
  66. }
  67. $iHeader = fgets(STDIN, 3);
  68. $aLength = unpack('n', $iHeader);
  69. $iLength = $aLength['1'];
  70. // No data? Then quit
  71. if ($iLength == 0) {
  72. $this->writeLog(LOG_ERR, 'we got no data, quitting');
  73. return;
  74. }
  75. // Fetching the data
  76. $sData = fgets(STDIN, $iLength + 1);
  77. $this->writeLog(LOG_DEBUG, 'received data: ' . $sData);
  78. $aCommand = explode(':', $sData);
  79. if (is_array($aCommand)) {
  80. switch ($aCommand[0]) {
  81. case 'isuser':
  82. // Check the existance of a given username
  83. $this->isUser($aCommand);
  84. break;
  85. case 'auth':
  86. // Check if the givven password is correct
  87. $this->auth($aCommand);
  88. break;
  89. case 'setpass':
  90. // We don't accept the setting of passwords here
  91. $this->writeLog(LOG_NOTICE, 'setpass command disabled');
  92. fwrite(STDOUT, pack('nn', 2, 0));
  93. break;
  94. default:
  95. // We don't know the given command
  96. $this->writeLog(LOG_NOTICE, 'unknown command ' . $aCommand[0]);
  97. fwrite(STDOUT, pack('nn', 2, 0));
  98. break;
  99. }
  100. } else {
  101. $this->writeLog(LOG_NOTICE, 'invalid command string ' . $sData);
  102. fwrite(STDOUT, pack('nn', 2, 0));
  103. }
  104. }
  105. }
  106. /**
  107. * @brief Check if the given username exists
  108. *
  109. * @param array $aCommand The command array
  110. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  111. */
  112. private function isUser(array $aCommand)
  113. {
  114. // Check if there is a username
  115. if (!isset($aCommand[1])) {
  116. $this->writeLog(LOG_NOTICE, 'invalid isuser command, no username given');
  117. fwrite(STDOUT, pack('nn', 2, 0));
  118. return;
  119. }
  120. // We only allow one process per hostname. So we set a lock file
  121. // Problem: We get the firstname after the first auth - not before
  122. $this->setHost($aCommand[2]);
  123. // Now we check if the given user is valid
  124. $sUser = str_replace(['%20', '(a)'], [' ', '@'], $aCommand[1]);
  125. // Does the hostname match? So we try directly
  126. if (DI::baseUrl()->getHostname() == $aCommand[2]) {
  127. $this->writeLog(LOG_INFO, 'internal user check for ' . $sUser . '@' . $aCommand[2]);
  128. $found = DBA::exists('user', ['nickname' => $sUser]);
  129. } else {
  130. $found = false;
  131. }
  132. // If the hostnames doesn't match or there is some failure, we try to check remotely
  133. if (!$found) {
  134. $found = $this->checkUser($aCommand[2], $aCommand[1], true);
  135. }
  136. if ($found) {
  137. // The user is okay
  138. $this->writeLog(LOG_NOTICE, 'valid user: ' . $sUser);
  139. fwrite(STDOUT, pack('nn', 2, 1));
  140. } else {
  141. // The user isn't okay
  142. $this->writeLog(LOG_WARNING, 'invalid user: ' . $sUser);
  143. fwrite(STDOUT, pack('nn', 2, 0));
  144. }
  145. }
  146. /**
  147. * @brief Check remote user existance via HTTP(S)
  148. *
  149. * @param string $host The hostname
  150. * @param string $user Username
  151. * @param boolean $ssl Should the check be done via SSL?
  152. *
  153. * @return boolean Was the user found?
  154. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  155. */
  156. private function checkUser($host, $user, $ssl)
  157. {
  158. $this->writeLog(LOG_INFO, 'external user check for ' . $user . '@' . $host);
  159. $url = ($ssl ? 'https' : 'http') . '://' . $host . '/noscrape/' . $user;
  160. $curlResult = Network::curl($url);
  161. if (!$curlResult->isSuccess()) {
  162. return false;
  163. }
  164. if ($curlResult->getReturnCode() != 200) {
  165. return false;
  166. }
  167. $json = @json_decode($curlResult->getBody());
  168. if (!is_object($json)) {
  169. return false;
  170. }
  171. return $json->nick == $user;
  172. }
  173. /**
  174. * @brief Authenticate the given user and password
  175. *
  176. * @param array $aCommand The command array
  177. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  178. */
  179. private function auth(array $aCommand)
  180. {
  181. // check user authentication
  182. if (sizeof($aCommand) != 4) {
  183. $this->writeLog(LOG_NOTICE, 'invalid auth command, data missing');
  184. fwrite(STDOUT, pack('nn', 2, 0));
  185. return;
  186. }
  187. // We only allow one process per hostname. So we set a lock file
  188. // Problem: We get the firstname after the first auth - not before
  189. $this->setHost($aCommand[2]);
  190. // We now check if the password match
  191. $sUser = str_replace(['%20', '(a)'], [' ', '@'], $aCommand[1]);
  192. // Does the hostname match? So we try directly
  193. if (DI::baseUrl()->getHostname() == $aCommand[2]) {
  194. $this->writeLog(LOG_INFO, 'internal auth for ' . $sUser . '@' . $aCommand[2]);
  195. $aUser = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'], ['nickname' => $sUser]);
  196. if (DBA::isResult($aUser)) {
  197. $uid = $aUser['uid'];
  198. $success = User::authenticate($aUser, $aCommand[3], true);
  199. $Error = $success === false;
  200. } else {
  201. $this->writeLog(LOG_WARNING, 'user not found: ' . $sUser);
  202. $Error = true;
  203. $uid = -1;
  204. }
  205. if ($Error) {
  206. $this->writeLog(LOG_INFO, 'check against alternate password for ' . $sUser . '@' . $aCommand[2]);
  207. $sPassword = DI::pConfig()->get($uid, 'xmpp', 'password', null, true);
  208. $Error = ($aCommand[3] != $sPassword);
  209. }
  210. } else {
  211. $Error = true;
  212. }
  213. // If the hostnames doesn't match or there is some failure, we try to check remotely
  214. if ($Error) {
  215. $Error = !$this->checkCredentials($aCommand[2], $aCommand[1], $aCommand[3], true);
  216. }
  217. if ($Error) {
  218. $this->writeLog(LOG_WARNING, 'authentification failed for user ' . $sUser . '@' . $aCommand[2]);
  219. fwrite(STDOUT, pack('nn', 2, 0));
  220. } else {
  221. $this->writeLog(LOG_NOTICE, 'authentificated user ' . $sUser . '@' . $aCommand[2]);
  222. fwrite(STDOUT, pack('nn', 2, 1));
  223. }
  224. }
  225. /**
  226. * @brief Check remote credentials via HTTP(S)
  227. *
  228. * @param string $host The hostname
  229. * @param string $user Username
  230. * @param string $password Password
  231. * @param boolean $ssl Should the check be done via SSL?
  232. *
  233. * @return boolean Are the credentials okay?
  234. */
  235. private function checkCredentials($host, $user, $password, $ssl)
  236. {
  237. $this->writeLog(LOG_INFO, 'external credential check for ' . $user . '@' . $host);
  238. $url = ($ssl ? 'https' : 'http') . '://' . $host . '/api/account/verify_credentials.json?skip_status=true';
  239. $ch = curl_init();
  240. curl_setopt($ch, CURLOPT_URL, $url);
  241. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  242. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
  243. curl_setopt($ch, CURLOPT_HEADER, true);
  244. curl_setopt($ch, CURLOPT_NOBODY, true);
  245. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
  246. curl_setopt($ch, CURLOPT_USERPWD, $user . ':' . $password);
  247. curl_exec($ch);
  248. $curl_info = @curl_getinfo($ch);
  249. $http_code = $curl_info['http_code'];
  250. curl_close($ch);
  251. $this->writeLog(LOG_INFO, 'external auth for ' . $user . '@' . $host . ' returned ' . $http_code);
  252. return $http_code == 200;
  253. }
  254. /**
  255. * @brief Set the hostname for this process
  256. *
  257. * @param string $host The hostname
  258. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  259. */
  260. private function setHost($host)
  261. {
  262. if (!empty($this->host)) {
  263. return;
  264. }
  265. $this->writeLog(LOG_INFO, 'Hostname for process ' . getmypid() . ' is ' . $host);
  266. $this->host = $host;
  267. $lockpath = Config::get('jabber', 'lockpath');
  268. if (is_null($lockpath)) {
  269. $this->writeLog(LOG_INFO, 'No lockpath defined.');
  270. return;
  271. }
  272. $file = $lockpath . DIRECTORY_SEPARATOR . $host;
  273. if (PidFile::isRunningProcess($file)) {
  274. if (PidFile::killProcess($file)) {
  275. $this->writeLog(LOG_INFO, 'Old process was successfully killed');
  276. } else {
  277. $this->writeLog(LOG_ERR, "The old Process wasn't killed in time. We now quit our process.");
  278. die();
  279. }
  280. }
  281. // Now it is safe to create the pid file
  282. PidFile::create($file);
  283. if (!file_exists($file)) {
  284. $this->writeLog(LOG_WARNING, 'Logfile ' . $file . " couldn't be created.");
  285. }
  286. }
  287. /**
  288. * @brief write data to the syslog
  289. *
  290. * @param integer $loglevel The syslog loglevel
  291. * @param string $sMessage The syslog message
  292. */
  293. private function writeLog($loglevel, $sMessage)
  294. {
  295. if (!$this->bDebug && ($loglevel >= LOG_DEBUG)) {
  296. return;
  297. }
  298. syslog($loglevel, $sMessage);
  299. }
  300. /**
  301. * @brief destroy the class, close the syslog connection.
  302. */
  303. public function __destruct()
  304. {
  305. $this->writeLog(LOG_NOTICE, 'stop');
  306. closelog();
  307. }
  308. }