1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
4 class MTrackAuth_HTTP implements IMTrackAuth {
5 public $htgroup = null;
6 public $htpasswd = null;
7 public $use_digest = false;
8 public $realm = 'mtrack';
10 function __construct($group = null, $passwd = null) {
11 $this->htgroup = $group;
12 if ($passwd !== null) {
13 if (!strncmp('digest:', $passwd, 7)) {
14 $this->use_digest = true;
15 $passwd = substr($passwd, 7);
17 $this->htpasswd = $passwd;
19 MTrackAuth::registerMech($this);
22 function parseDigest($string)
24 $resp = trim($string);
26 while (strlen($resp)) {
27 if (!preg_match('/^([a-z-]+)\s*=\s*(.*)$/', $resp, $M)) {
28 # error_log("unable to parse $string [$resp]");
36 if ($rest[0] == '"' || $rest[0] == "'") {
46 if ($delim != ',' && $rest[$i] == '\\') {
49 # error_log("unable to parse $string (unterminated quotes)");
54 if ($rest[$i] == $delim) {
55 $param = substr($rest, $delim_offset, $i - $delim_offset);
56 $resp = substr($rest, $i + 1);
61 if ($param === null && $delim != ',') {
62 # error_log("unable to parse $string, unterminated delim $delim");
65 if ($param === null) {
71 if (preg_match('/^,\s*(.*)$/', $resp, $M)) {
79 /* Leave authentication to the web server configuration */
80 function authenticate() {
81 /* web server based auth */
82 if (isset($_SERVER['REMOTE_USER'])) {
83 return $_SERVER['REMOTE_USER'];
87 if (($this->use_digest && isset($_SERVER['PHP_AUTH_DIGEST'])) ||
88 (!$this->use_digest && isset($_SERVER['PHP_AUTH_USER'])))
90 /* validate the password */
91 if ($this->use_digest) {
92 /* parse the digest response */
94 $DIG = $this->parseDigest($_SERVER['PHP_AUTH_DIGEST']);
96 if ($DIG['nc'] != '00000001') {
97 // only allow a nonce-count of 1
100 if ($DIG['realm'] != $this->realm) {
103 $secret = $this->getSecret();
106 $opaque = sha1($domain . $secret);
108 if ($DIG['opaque'] != $opaque) {
110 error_log("secret expired");
114 $user = $DIG['username'];
117 $user = $_SERVER['PHP_AUTH_USER'];
120 if (!strlen($user)) {
124 if ($this->htpasswd === null) {
125 error_log("no password file defined, unable to validate $user");
129 $fp = fopen($this->htpasswd, 'r');
131 error_log("unable to open password file to validate user $user");
135 if (!flock($fp, LOCK_SH)) {
136 error_log("unable to lock password file to validate user $user");
140 $puser = preg_quote($user);
141 $correct_password = null;
145 if ($line === false) {
150 if ($this->use_digest) {
151 if (preg_match("/^$puser:(.*):(.*)$/", $line, $M)) {
152 if ($M[1] != $this->realm) {
155 // $M[2] is: md5($user . ":" . $realm . ":" . $pw)
157 $uri = md5($_SERVER['REQUEST_METHOD'] . ':' . $DIG['uri']);
158 $resp = md5("$expect:$DIG[nonce]:$DIG[nc]:$DIG[cnonce]:$DIG[qop]:$uri");
159 if ($resp != $DIG['response']) {
166 if (preg_match("/^$puser\s*:\s*(\S+)/", $line, $M)) {
167 if (crypt($_SERVER['PHP_AUTH_PW'], $M[1]) != $M[1]) {
184 function getSecret() {
185 $secret_file = MTrackConfig::get('core', 'vardir') . '/.digest.secret';
186 if (file_exists($secret_file)) {
187 if (filemtime($secret_file) + 300 > time()) {
188 $res = file_get_contents($secret_file);
189 if ($res === false) {
191 "Unable to read HTTP secret for mtrack; logins will likely fail");
195 unlink($secret_file);
198 if (!file_put_contents($secret_file, $secret)) {
200 "Unable to write HTTP secret for mtrack; logins will likely fail");
205 function doAuthenticate($force = false) {
206 /* This is only triggered if the web server isn't configured
207 * to handle auth itself */
209 $realm = $this->realm;
211 if ($this->use_digest) {
212 $secret = $this->getSecret();
213 $nonce = sha1(uniqid() . $secret);
216 $opaque = sha1($domain . $secret);
217 header("WWW-Authenticate: Digest realm=\"$realm\",qop=\"auth\",nonce=\"$nonce\",opaque=\"$opaque\"");
219 header("WWW-Authenticate: Basic realm=\"$realm\"");
221 header('HTTP/1.0 401 Unauthorized');
224 <h1>Authentication Required</h1>
226 <p>I need to know who you are to allow you to access to this site.</p>
231 protected function readGroupFile($filename) {
232 if (!file_exists($filename)) return null;
233 $fp = fopen($filename, 'r');
234 if (!$fp) return null;
235 if (!flock($fp, LOCK_SH)) return null;
237 /* an apache style group file */
243 if ($line === false) {
247 if ($line[0] == '#') {
250 if (preg_match('/^([a-zA-Z][a-zA-Z0-9_]+)\s*:\s*(.*)$/', $line,
254 foreach (preg_split('/\s+/', $members) as $user) {
255 $users[$user][] = $groupname;
256 $groups[$groupname][] = $user;
263 return array($groups, $users);
266 function enumGroups() {
267 if (strlen($this->htgroup)) {
268 list($groups, $users) = $this->readGroupFile($this->htgroup);
269 return array_keys($groups);
274 function getGroups($username) {
275 if (strlen($this->htgroup)) {
276 list($groups, $users) = $this->readGroupFile($this->htgroup);
277 return $users[$username];
282 function addToGroup($username, $groupname)
287 function removeFromGroup($username, $groupname)
292 function getUserData($username) {
296 /** a bit of a hack; this helper enables the HTTP password to be set
297 * by the user admin screen */
298 function setUserPassword($username, $password) {
299 if (!$this->use_digest) {
300 throw new Exception("not supported");
302 $pwline = "mtrack:" .
303 md5("$username:mtrack:" . $password);
304 $fp = fopen($this->htpasswd, 'r+');
305 if (!$fp && !file_exists($this->htpasswd)) {
306 $fp = fopen($this->htpasswd, 'w');
309 throw new Exception("failed to write to $this->htpasswd");
313 while (($line = fgets($fp)) !== false) {
314 $bits = explode(':', $line, 2);
315 if (count($bits) >= 2) {
316 $lines[$bits[0]] = $bits[1];
319 $lines[$username] = $pwline;
322 foreach ($lines as $user => $rest) {
323 fwrite($fp, "$user:$rest\n");