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';
5 class MTrackAuth_HTTP implements IMTrackAuth {
6 public $htgroup = null;
7 public $htpasswd = null;
8 public $use_digest = false;
9 public $realm = 'mtrack';
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);
18 $this->htpasswd = $passwd;
20 MTrackAuth::registerMech($this);
23 function parseDigest($string)
25 $resp = trim($string);
27 while (strlen($resp)) {
28 if (!preg_match('/^([a-z-]+)\s*=\s*(.*)$/', $resp, $M)) {
29 # error_log("unable to parse $string [$resp]");
37 if ($rest[0] == '"' || $rest[0] == "'") {
47 if ($delim != ',' && $rest[$i] == '\\') {
50 # error_log("unable to parse $string (unterminated quotes)");
55 if ($rest[$i] == $delim) {
56 $param = substr($rest, $delim_offset, $i - $delim_offset);
57 $resp = substr($rest, $i + 1);
62 if ($param === null && $delim != ',') {
63 # error_log("unable to parse $string, unterminated delim $delim");
66 if ($param === null) {
72 if (preg_match('/^,\s*(.*)$/', $resp, $M)) {
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'];
88 if (($this->use_digest && isset($_SERVER['PHP_AUTH_DIGEST'])) ||
89 (!$this->use_digest && isset($_SERVER['PHP_AUTH_USER'])))
91 /* validate the password */
92 if ($this->use_digest) {
93 /* parse the digest response */
95 $DIG = $this->parseDigest($_SERVER['PHP_AUTH_DIGEST']);
97 if ($DIG['nc'] != '00000001') {
98 // only allow a nonce-count of 1
101 if ($DIG['realm'] != $this->realm) {
104 $secret = $this->getSecret();
107 $opaque = sha1($domain . $secret);
109 if ($DIG['opaque'] != $opaque) {
111 error_log("secret expired");
115 $user = $DIG['username'];
118 $user = $_SERVER['PHP_AUTH_USER'];
121 if (!strlen($user)) {
125 if ($this->htpasswd === null) {
126 error_log("no password file defined, unable to validate $user");
130 $fp = fopen($this->htpasswd, 'r');
132 error_log("unable to open password file to validate user $user");
136 if (!flock($fp, LOCK_SH)) {
137 error_log("unable to lock password file to validate user $user");
141 $puser = preg_quote($user);
142 $correct_password = null;
146 if ($line === false) {
151 if ($this->use_digest) {
152 if (preg_match("/^$puser:(.*):(.*)$/", $line, $M)) {
153 if ($M[1] != $this->realm) {
156 // $M[2] is: md5($user . ":" . $realm . ":" . $pw)
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']) {
167 if (preg_match("/^$puser\s*:\s*(\S+)/", $line, $M)) {
168 if (crypt($_SERVER['PHP_AUTH_PW'], $M[1]) != $M[1]) {
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) {
192 "Unable to read HTTP secret for mtrack; logins will likely fail");
196 unlink($secret_file);
199 if (!file_put_contents($secret_file, $secret)) {
201 "Unable to write HTTP secret for mtrack; logins will likely fail");
206 function doAuthenticate($force = false) {
207 /* This is only triggered if the web server isn't configured
208 * to handle auth itself */
210 $realm = $this->realm;
212 if ($this->use_digest) {
213 $secret = $this->getSecret();
214 $nonce = sha1(uniqid() . $secret);
217 $opaque = sha1($domain . $secret);
218 header("WWW-Authenticate: Digest realm=\"$realm\",qop=\"auth\",nonce=\"$nonce\",opaque=\"$opaque\"");
220 header("WWW-Authenticate: Basic realm=\"$realm\"");
222 header('HTTP/1.0 401 Unauthorized');
225 <h1>Authentication Required</h1>
227 <p>I need to know who you are to allow you to access to this site.</p>
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;
238 /* an apache style group file */
244 if ($line === false) {
248 if ($line[0] == '#') {
251 if (preg_match('/^([a-zA-Z][a-zA-Z0-9_]+)\s*:\s*(.*)$/', $line,
255 foreach (preg_split('/\s+/', $members) as $user) {
256 $users[$user][] = $groupname;
257 $groups[$groupname][] = $user;
264 return array($groups, $users);
267 function enumGroups() {
268 if (strlen($this->htgroup)) {
269 list($groups, $users) = $this->readGroupFile($this->htgroup);
270 return array_keys($groups);
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();
283 function addToGroup($username, $groupname)
288 function removeFromGroup($username, $groupname)
293 function getUserData($username) {
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");
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');
310 throw new Exception("failed to write to $this->htpasswd");
314 while (($line = fgets($fp)) !== false) {
315 $bits = explode(':', $line, 2);
316 if (count($bits) >= 2) {
317 $lines[$bits[0]] = $bits[1];
320 $lines[$username] = $pwline;
323 foreach ($lines as $user => $rest) {
324 fwrite($fp, "$user:$rest\n");