Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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