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