final move of files
[web.mtrack] / MTrack / auth / http.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3 require_once 'MTrack/Interface/Auth.php';
4
5 class MTrackAuth_HTTP implements IMTrackAuth {
6   public $htgroup = null;
7   public $htpasswd = null;
8   public $use_digest = false;
9   public $realm = 'mtrack';
10
11   function __construct($group = null, $passwd = null) {
12     $this->htgroup = $group;
13     if ($passwd !== null) {
14       if (!strncmp('digest:', $passwd, 7)) {
15         $this->use_digest = true;
16         $passwd = substr($passwd, 7);
17       }
18       $this->htpasswd = $passwd;
19     }
20     MTrackAuth::registerMech($this);
21   }
22
23   function parseDigest($string)
24   {
25     $resp = trim($string);
26     $DIG = array();
27     while (strlen($resp)) {
28       if (!preg_match('/^([a-z-]+)\s*=\s*(.*)$/', $resp, $M)) {
29 #        error_log("unable to parse $string [$resp]");
30         return null;
31       }
32       $name = $M[1];
33       $param = null;
34
35       $rest = $M[2];
36
37       if ($rest[0] == '"' || $rest[0] == "'") {
38         $delim = $rest[0];
39         $delim_offset = 1;
40       } else {
41         $delim = ',';
42         $delim_offset = 0;
43       }
44       $len = strlen($rest);
45       $i = $delim_offset;
46       while ($i < $len) {
47         if ($delim != ',' && $rest[$i] == '\\') {
48           $i += 2;
49           if ($i >= $len) {
50 #            error_log("unable to parse $string (unterminated quotes)");
51             return null;
52           }
53           continue;
54         }
55         if ($rest[$i] == $delim) {
56           $param = substr($rest, $delim_offset, $i - $delim_offset);
57           $resp = substr($rest, $i + 1);
58           break;
59         }
60         $i++;
61       }
62       if ($param === null && $delim != ',') {
63 #        error_log("unable to parse $string, unterminated delim $delim");
64         return null;
65       }
66       if ($param === null) {
67         $param = $rest;
68         $resp = '';
69       }
70       $DIG[$name] = $param;
71
72       if (preg_match('/^,\s*(.*)$/', $resp, $M)) {
73         $resp = $M[1];
74       }
75       $resp = trim($resp);
76     }
77     return $DIG;
78   }
79
80   /* Leave authentication to the web server configuration */
81   function authenticate() {
82     /* web server based auth */
83     if (isset($_SERVER['REMOTE_USER'])) {
84       return $_SERVER['REMOTE_USER'];
85     }
86
87     /* PHP based auth */
88     if (($this->use_digest && isset($_SERVER['PHP_AUTH_DIGEST'])) ||
89         (!$this->use_digest && isset($_SERVER['PHP_AUTH_USER'])))
90     {
91       /* validate the password */
92       if ($this->use_digest) {
93         /* parse the digest response */
94
95         $DIG = $this->parseDigest($_SERVER['PHP_AUTH_DIGEST']);
96
97         if ($DIG['nc'] != '00000001') {
98           // only allow a nonce-count of 1
99           return null;
100         }
101         if ($DIG['realm'] != $this->realm) {
102           return null;
103         }
104         $secret = $this->getSecret();
105         global $ABSWEB;
106         $domain = $ABSWEB;
107         $opaque = sha1($domain . $secret);
108
109         if ($DIG['opaque'] != $opaque) {
110           // secret expired
111           error_log("secret expired");
112           return null;
113         }
114
115         $user = $DIG['username'];
116
117       } else {
118         $user = $_SERVER['PHP_AUTH_USER'];
119       }
120
121       if (!strlen($user)) {
122         return null;
123       }
124
125       if ($this->htpasswd === null) {
126         error_log("no password file defined, unable to validate $user");
127         return null;
128       }
129
130       $fp = fopen($this->htpasswd, 'r');
131       if (!$fp) {
132         error_log("unable to open password file to validate user $user");
133         return null;
134       }
135
136       if (!flock($fp, LOCK_SH)) {
137         error_log("unable to lock password file to validate user $user");
138         return null;
139       }
140
141       $puser = preg_quote($user);
142       $correct_password = null;
143
144       while (true) {
145         $line = fgets($fp);
146         if ($line === false) {
147           $user = false;
148           break;
149         }
150
151         if ($this->use_digest) {
152           if (preg_match("/^$puser:(.*):(.*)$/", $line, $M)) {
153             if ($M[1] != $this->realm) {
154               continue;
155             }
156             // $M[2] is: md5($user . ":" . $realm . ":" . $pw)
157             $expect = $M[2];
158             $uri = md5($_SERVER['REQUEST_METHOD'] . ':' . $DIG['uri']);
159             $resp = md5("$expect:$DIG[nonce]:$DIG[nc]:$DIG[cnonce]:$DIG[qop]:$uri");
160             if ($resp != $DIG['response']) {
161               /* invalid */
162               $user = null;
163             }
164             break;
165           }
166         } else {
167           if (preg_match("/^$puser\s*:\s*(\S+)/", $line, $M)) {
168             if (crypt($_SERVER['PHP_AUTH_PW'], $M[1]) != $M[1]) {
169               /* invalid */
170               $user = null;
171             }
172             break;
173           }
174         }
175       }
176       flock($fp, LOCK_UN);
177       $fp = null;
178
179       return $user;
180     }
181
182     return null;
183   }
184
185   function getSecret() {
186     $secret_file = MTrackConfig::get('core', 'vardir') . '/.digest.secret';
187     if (file_exists($secret_file)) {
188       if (filemtime($secret_file) + 300 > time()) {
189         $res = file_get_contents($secret_file);
190         if ($res === false) {
191           error_log(
192             "Unable to read HTTP secret for mtrack; logins will likely fail");
193         }
194         return $res;
195       }
196       unlink($secret_file);
197     }
198     $secret = uniqid();
199     if (!file_put_contents($secret_file, $secret)) {
200       error_log(
201         "Unable to write HTTP secret for mtrack; logins will likely fail");
202     }
203     return $secret;
204   }
205
206   function doAuthenticate($force = false) {
207     /* This is only triggered if the web server isn't configured
208      * to handle auth itself */
209
210     $realm = $this->realm;
211
212     if ($this->use_digest) {
213       $secret = $this->getSecret();
214       $nonce = sha1(uniqid() . $secret);
215       global $ABSWEB;
216       $domain = $ABSWEB;
217       $opaque = sha1($domain . $secret);
218       header("WWW-Authenticate: Digest realm=\"$realm\",qop=\"auth\",nonce=\"$nonce\",opaque=\"$opaque\"");
219     } else {
220       header("WWW-Authenticate: Basic realm=\"$realm\"");
221     }
222     header('HTTP/1.0 401 Unauthorized');
223
224 ?>
225 <h1>Authentication Required</h1>
226
227 <p>I need to know who you are to allow you to access to this site.</p>
228 <?php
229     exit;
230   }
231
232   protected function readGroupFile($filename) {
233     if (!file_exists($filename)) return null;
234     $fp = fopen($filename, 'r');
235     if (!$fp) return null;
236     if (!flock($fp, LOCK_SH)) return null;
237
238     /* an apache style group file */
239     $groups = array();
240     $users = array();
241
242     while (true) {
243       $line = fgets($fp);
244       if ($line === false) {
245         break;
246       }
247       $line = trim($line);
248       if ($line[0] == '#') {
249         continue;
250       }
251       if (preg_match('/^([a-zA-Z][a-zA-Z0-9_]+)\s*:\s*(.*)$/', $line,
252             $M)) {
253         $groupname = $M[1];
254         $members = $M[2];
255         foreach (preg_split('/\s+/', $members) as $user) {
256           $users[$user][] = $groupname;
257           $groups[$groupname][] = $user;
258         }
259       }
260     }
261
262     flock($fp, LOCK_UN);
263     $fp = null;
264     return array($groups, $users);
265   }
266
267   function enumGroups() {
268     if (strlen($this->htgroup)) {
269       list($groups, $users) = $this->readGroupFile($this->htgroup);
270       return array_keys($groups);
271     }
272     return null;
273   }
274
275   function getGroups($username) {
276     if (strlen($this->htgroup)) {
277       list($groups, $users) = $this->readGroupFile($this->htgroup);
278       return isset($users[$username]) ? $users[$username] : array();
279     }
280     return null;
281   }
282
283   function addToGroup($username, $groupname)
284   {
285     return null;
286   }
287
288   function removeFromGroup($username, $groupname)
289   {
290     return null;
291   }
292
293   function getUserData($username) {
294     return null;
295   }
296
297   /** a bit of a hack; this helper enables the HTTP password to be set
298    * by the user admin screen */
299   function setUserPassword($username, $password) {
300     if (!$this->use_digest) {
301       throw new Exception("not supported");
302     }
303     $pwline = "mtrack:" .
304       md5("$username:mtrack:" . $password);
305     $fp = fopen($this->htpasswd, 'r+');
306     if (!$fp && !file_exists($this->htpasswd)) {
307       $fp = fopen($this->htpasswd, 'w');
308     }
309     if (!$fp) {
310       throw new Exception("failed to write to $this->htpasswd");
311     }
312     flock($fp, LOCK_EX);
313     $lines = array();
314     while (($line = fgets($fp)) !== false) {
315       $bits = explode(':', $line, 2);
316       if (count($bits) >= 2) {
317         $lines[$bits[0]] = $bits[1];
318       }
319     }
320     $lines[$username] = $pwline;
321     fseek($fp, 0);
322     ftruncate($fp, 0);
323     foreach ($lines as $user => $rest) {
324       fwrite($fp, "$user:$rest\n");
325     }
326     flock($fp, LOCK_UN);
327     $fp = null;
328   }
329 }
330