<?
# Copyright (c) 2005 JP Sugarbroad
# Permission is granted to use this in any way you want, provided that you
# include the above copyright notice (or one with similar effect) in any work
# including non-trivial parts of this one.
#
# UI by nandhp.

# Change history:
#       20050706.0: Fixed XTEA routines and signatures
#       20050707.0: More XTEA fixes
#       20050707.1: valid_root fix (thanks to meepbear)
#       20050708.0: Fix cookie check
#       20050708.1: Remove issued field from token
#       20050713.0: Update to new protocol, added example login code

define("SIGKEY""foobar");
define("COOKIE_NAME""secret");
define("COOKIE_VALUE""ackbar");
define("VALID_TIME"60); # token validity time
define("ASSOC_TIME"15 60); # assocation validity time
define("KEY_LEN"20); # Don't touch me

function t2utc($t) {
    return 
gmdate("Y-m-d\\TH:i:s\\Z"$t);
}

function 
utc2t($s) {
    return 
strtotime(str_replace("T"" "$s));
}

# * is NOT OK
# *.x is NOT OK
# *.x.<tld_list non-element> is NOT OK
# Anything else is OK
# List from: http://www.iana.org/gtld/gtld.htm
$tld_list = array(
    
"aero" => 1,
    
"biz" => 1,
    
"com" => 1,
    
"coop" => 1,
    
"edu" => 1,
    
"gov" => 1,
    
"info" => 1,
    
"int" => 1,
    
"mil" => 1,
    
"museum" => 1,
    
"name" => 1,
    
"net" => 1,
    
"org" => 1,
    
"pro" => 1);

function 
valid_wildcard($h) {
    switch (
strrpos($h"*")) {
    case 
false:
        
# Not wildcard
        
return true;
    case 
0:
        
# Wildcard
        
break;
    default:
        
# * not at start
        
return false;
    }
    
$h explode("."$h);
    if (
$h[0] != "*") return false# *xyz.stuff is bad
    
switch (count($h)) {
    case 
0:
    case 
1:
    case 
2:
        return 
false;
    case 
3:
        return 
array_key_exists($h[-1], $tld_list);
    default:
        return 
true;
    }
}

function 
valid_root($root$ret) {
    
$root parse_url($root);
    if (isset(
$root["fragment"])) return false;
    
$ret parse_url($ret);
    if (
$root["scheme"] != $ret["scheme"]) return false;
    if (
$root["port"] != $ret["port"]) return false;
    if (isset(
$root["user"]) && $root["user"] != $ret["user"]) return false;
    if (isset(
$root["pass"]) && $root["pass"] != $ret["pass"]) return false;
    if (isset(
$root["query"]) && $root["query"] != $ret["query"]) return false;
    
$h $root["host"];
    if (!
valid_wildcard($h)) return false;
    if (
$h[0] == "*") {
        
$hn strlen($h) - 1;
        if (
substr($h2) != substr($ret["host"], -$hn$hn)) return false;
    } else {
        if (
$h != $ret["host"]) return false;
    }
    
$p1 explode("/"rtrim($root["path"], "/"));
    
$p2 explode("/"rtrim($ret["path"], "/"));
    foreach (
$p1 as $k => $v) {
        if (
$p2[$k] != $v) return false;
    }
    return 
true;
}

function 
randbytes($n) {
    
$r fopen("/dev/urandom""rb");
    
$s fread($r$n);
    
fclose($r);
    return 
$s;
}

function 
xtea_block($k$v) {
    list(, 
$v0$v1) = unpack("N*"$v);
    
$sum 0;
    
$delta 0x9E3779B9;
    for (
$i 0$i 32$i++) {
        
$v0 intval($v0 + ($v1 << $v1 >> 5) + $v1 $sum $k[$sum 3]);
        
$sum intval($sum $delta);
        
$v1 intval($v1 + ($v0 << $v0 >> 5) + $v0 $sum $k[$sum >> 11 3]);
    }
    return 
pack("N2"$v0$v1);
}

function 
xtea_encrypt($key$data) {
    
$key array_merge(unpack("N*"str_pad($key16chr(0))));
    
$v randbytes(8);
    
$out $v;
    
$i 0;
    
$l strlen($data);
    while (
$i $l) {
        
$v xtea_block($key$v);
        
$p substr($data$i8);
        
$i += 8;
        
$v ^= $p;
        
$out .= $v;
    }
    return 
$out;
}

function 
xtea_decrypt($key$data) {
    
$key array_merge(unpack("N*"str_pad($key16chr(0))));
    
$v substr($data08);
    
$i 8;
    
$l strlen($data);
    
$out "";
    while (
$i $l) {
        
$v xtea_block($key$v);
        
$c substr($data$i8);
        
$i += 8;
        
$out .= $v $c;
        
$v $c;
    }
    return 
$out;
}

define("HASH_LEN"20);
function 
hmac($key$str) {
    
$key str_pad($key64chr(0));
    
$ipad $key str_repeat(chr(0x36), 64);
    
$opad $key str_repeat(chr(0x5C), 64);

    return 
pack("H*"sha1($opad pack("H*"sha1($ipad $str))));
}

function 
sign_array($key$data) {
    
$token "";
    foreach (
explode(","$data["signed"]) as $f) {
        
$token .= "$f:$data[$f]\n";
    }
    return 
base64_encode(hmac($key$token));
}

function 
make_handle($expiry$exposed$key) {
    
$token pack("lc"$expiry$exposed 0) . $key;
    return 
base64_encode(xtea_encrypt(SIGKEYhmac(SIGKEY$token) . $token));
}

function 
check_handle($bh$exposed_ok) {
    
$handle base64_decode($bh);
    
# IV + HMAC + expiry + exposed
    
if (!$handle || strlen($handle) < HASH_LEN 5) return false;
    
$handle xtea_decrypt(SIGKEY$handle);
    
$data substr($handleHASH_LEN);
    if (
hmac(SIGKEY$data) != substr($handle0HASH_LEN)) return false;
    
$t unpack("lexpiry/cexposed"$data);
    if (
$t["expiry"] < time() || $t["exposed"] && !$exposed_ok) return false;
    return 
substr($data5);
}

function 
make_args($prefix$data) {
    
$url "";
    foreach (
$data as $k => $v) {
        
$url .= "$prefix$k=" urlencode($v) . "&";
    }
    return 
rtrim($url"&");
}

function 
continuation() {
    
$url "";
    foreach (
$_REQUEST as $k => $v) {
        if (
strncmp($k"openid_"7) || $k == "openid_mode") continue;
        
$url .= "&openid." substr($k7) . "=" urlencode($v);
    }
    return 
$url;
}

$ret $_REQUEST["openid_return_to"];
if (
$ret) {
    if (!
preg_match("/^https?:/"$ret)) {
        
header("HTTP/1.0 400 Bad Request");
        
?><html>
            <head><title>Bad Request</title></head>
            <body><p>The OpenID endpoint received an invalid request.</p></body>
        </html><?
        
exit;
    }
    if (
strchr($ret"?")) {
        
$retp "$ret&";
    } else {
        
$retp "$ret?";
    }
}

$self "http://" $_SERVER["SERVER_NAME"];
if (
$_SERVER["SERVER_PORT"] != 80)
    
$self .= ":" $SERVER["SERVER_PORT"];
$self .= "$_SERVER[PHP_SELF]?";

function 
badreq($msg) {
    global 
$ret$retp;
    if (
$_SERVER["REQUEST_METHOD"] == "POST") {
        
header("HTTP/1.0 400 Bad Request");
        print 
"error:$msg\n";
        exit;
    }
    if (
$ret) {
        
header("Location: " $retp "openid.mode=error&openid.error=" urlencode($msg));
    } else {
        
header("HTTP/1.0 400 Bad Request");
    }
template_header();
    
?><div class='linkbar'>This is an OpenID server endpoint, not a human-readable resource. For more information, see <a href="http://openid.net/">http://openid.net/</a>.</div>
        <? if ($msg) { ?>
            <h2>Error</h2><P>An error occurred processing your request: <?=htmlspecialchars($msg)?></body>
        <? }
template_footer();
    exit;
}

$mode $_REQUEST["openid_mode"];
switch (
$mode) {
case 
"check_authentication":
    
$resp = array();
    
$sig $_REQUEST["openid_signed"];
    
$resp["signed"] = $sig;
    foreach (
explode(","$sig) as $f) {
        
$resp[$f] = $_REQUEST["openid_" $f];
    }
    
$resp["mode"] = "id_res";

    
header("Content-Type: text/plain");
    
$key check_handle($_REQUEST["openid_assoc_handle"], false);
    if (
$key && $_REQUEST["openid_sig"] == sign_array($key$resp)) {
        
$l max(0utc2t($resp["valid_to"]) - time());
        print 
"is_valid:" . ($l 0) . "\nlifetime:" $l "\n";
    } else {
        print 
"is_valid:0\nlifetime:0\n";
    }
    
$ih $_REQUEST["openid_invalidate_handle"];
    if (
$ih && !check_handle($ihtrue)) {
        print 
"invalidate_handle:$ih\n";
    }
    exit;
case 
"associate":
    
$t $_REQUEST["openid_assoc_type"];
    if (isset(
$t) && $t != "HMAC-SHA1"badreq("Unknown association type");
    
$t time();
    
$e $t ASSOC_TIME;
    
$r randbytes(KEY_LEN);
    
$handle make_handle($etrue$r);
    
header("Content-Type: text/plain");
    print 
"assoc_type:HMAC-SHA1\nassoc_handle:" $handle .
          
"\nissued:" t2utc($t) . # COMPAT
          
"\nexpiry:" t2utc($e) . # COMPAT
          
"\nexpires_in:" ASSOC_TIME .
          
"\nmac_key:" base64_encode($r) .
          
"\n";
    exit;
case 
"login":
case 
"checkid_immediate":
case 
"checkid_setup":
    if (
$_SERVER["REQUEST_METHOD"] != "GET") {
        
badreq("Mode $mode requires GET method");
    }
    break;
case 
"dologin":
    break;
case 
null:
    
userinterface();#badreq(null);
    
exit;
default:
    
badreq("Unknown mode $mode");
}

if (!
$ret && $mode != "dologin" badreq("return_to required");
# If we have checkid_setup issue a redirect with mode login, then we don't
# have to make this a function. But that's an extra redirect we don't need.
function login() {
    global 
$mode$retp,$ret;
    
# TODO have the user log in
    
$pw file("openid_password");
    if ( 
$_REQUEST[password] && md5($_REQUEST[password]) == trim($pw[0]) ) {
                                                
#sec*min*hr*dy*yrs
      
setcookie(COOKIE_NAMECOOKIE_VALUEtime()+60*60*24*365*100);
      
$_COOKIE[COOKIE_NAME] = COOKIE_VALUE;
      
$mode "checkid_immediate";
    }
    else if ( 
$_COOKIE[COOKIE_NAME] != COOKIE_VALUE ) {
      
#header("Location: " . $retp . "openid.mode=cancel");
      
template_header('login');
      
$root $_REQUEST["openid_trust_root"];
      if (!
$root || !valid_root($root$ret)) {
    
$root $ret;
      }
      
$url "http://$_SERVER[SERVER_NAME]:$_SERVER[SERVER_PORT]$_SERVER[PHP_SELF]?openid.mode=dologin" . (is_trusted($root)?'':'&return_to=ui'). continuation();
      if ( !
$_SERVER[QUERY_STRING] ) echo "<div class='linkbar'>This is an OpenID server endpoint, not a human-readable resource. For more information, see <a href=\"http://www.openid.net/\">http://www.openid.net/</a>.</div>";
      
?>
<form action="<? echo $url ?>" method="post">
<fieldset>
<h2>Login</h2>
<p><? echo $_REQUEST[password]?'<span style="color:red">Your password is incorrect. Please try again.</span>':'You must login to OpenID to continue.'?></p>
   <p>Password: <input type="password" name="password"></p>
 <input type=submit value="Continue">
</fieldset>
</form>
<?
   template_footer
();
      exit;
    }
    
# } else if (login_canceled) {
    #     header("Location: " . $retp . "openid.mode=cancel");
    #     exit;
    # } else {
    #     request_login
    #     exit;
    # }
}
if (
$mode == "login" || $mode == "dologin"login();

if ( 
$_REQUEST['return_to'] == "ui" ) {
userinterface();
exit;
}

function 
request_ui() {
global 
$retp;
        
$url "http://$_SERVER[SERVER_NAME]:$_SERVER[SERVER_PORT]$_SERVER[PHP_SELF]?openid.mode=login&return_to=ui" continuation();
        
$url $retp "openid.mode=id_res&openid.user_setup_url=" urlencode($url);
        
header("Location: $url");
        
?><html>
            <head><title>Login required</title></head>
            <body><p>You need to <a href="<?=htmlspecialchars($url)?>">log in</a> to be authenticated.</p></body>
        </html><?
        
exit;
}

if (
$_COOKIE[COOKIE_NAME] != COOKIE_VALUE) {
    if (
$mode == "checkid_setup") {
        
login();
    } else {
request_ui();
    }
}

$id $_REQUEST["openid_identity"];
#if ($id != "http://taral.net/") badreq("Unrecognized identity");

$root $_REQUEST["openid_trust_root"];
if (!
$root || !valid_root($root$ret)) {
    
$root $ret;
}

function 
trim_in_place(&$value) { $value trim($value); }
function 
list_trusted_sites() {
$trusted file("openid_trust");
array_walk($trusted,'trim_in_place');
return 
$trusted;
}
function 
is_trusted($checkroot) {
$trusted list_trusted_sites();
foreach ( 
$trusted as $check ) {
if ( 
trim($check) == trim($checkroot) ) return 1;
}
return 
0;
}

if ( !
is_trusted($root) ) {
#badreq("Requester not trusted");
request_ui();
}
#switch ($root) {
#    case "http://www.lifewiki.net/":
#    case "http://*.danga.com/openid/demo/":
#        break;
#    default:
#        badreq("Requester not trusted");
#}

$t time();
$resp = array(
    
"mode" => "id_res",
    
"identity" => $id,
    
"issued" => t2utc($t), # COMPAT
    
"valid_to" => t2utc($t VALID_TIME),
    
"return_to" => $ret,
    
"signed" => "mode,issued,valid_to,identity,return_to");
$handle $_REQUEST["openid_assoc_handle"];
if (
$handle) {
    
$key check_handle($handletrue);
    if (
$key == false$resp["invalidate_handle"] = $handle;
}
if (!
$key) {
    
$key randbytes(KEY_LEN);
    
$handle make_handle($t ASSOC_TIMEfalse$key);
}
$resp["sig"] = sign_array($key$resp);
$resp["assoc_handle"] = $handle;
$url $retp make_args("openid."$resp);
header("Location: $url");
?><html>
    <head><title>Authentication OK</title></head>
    <body><p>Authentication complete. <a href="<?=htmlspecialchars($url)?>">Click here to proceed</a></p></body>
</html>
<?
#an actually distributed identity systema
function template_header($title='') {?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html>


<head>
   <title>OpenID<?echo $title?": $title":''?></title>
   <link rel='stylesheet' type='text/css' href='openid.css'>
   <link rel='shortcut icon' href='./favicon.ico'>

</head>
<body>
<h1><a href="openid.php"><img src='openid-logo.png' width='320' height='120' alt='OpenID' border='0' /></a></h1>
<?
}
function 
template_footer() { ?>
<div class='footer' style="text-align:center">This OpenID server is not affiliated in any way with <a href="http://www.openid.net/">OpenID</a>. PHP OpenID server copyright &copy; 2005 <a href="http://www.livejournal.com/users/taral/147710.html">JP Sugarbroad</a>.</div>
</body>
</html>
<? }

# User-Interface
function userinterface() {
   if ( 
$_REQUEST['logout'] ) {
      
setcookie(COOKIE_NAME''0);
      
template_header('preferences');
?>
<h2>Logout</h2>
You are now logged out. Have a nice day.
<p><a href="openid.php">Log in again</a></p>
<?
      template_footer
();
      exit;
   }
   
login();
   
template_header('preferences');
?>
<div class='linkbar'>
[<a href="openid.php" class='barlink'>Home</a>]
[<a href="openid.php?ui=trust" class='barlink'>Trust</a>]
&mdash;
[<a href="openid.php?logout=1" class='barlink'>Logout</a>]
</div>
<?
$ret 
$_REQUEST["openid_return_to"];
$root $_REQUEST["openid_trust_root"];
if ((!
$root || !valid_root($root$ret)) && !$_REQUEST['del_site']) {
    
$root $ret;
}

if ( 
$root && $_REQUEST['add_site'] ) {
if ( !
is_trusted($root) ) {
    
$out fopen("openid_trust","a");
    
fwrite($out,$root."\n");
    
fclose($out);
}

echo 
'<h2>Trust Site</h2>';
echo 
'<div style="text-align:center;font-style:italic;margin-bottom:.7em;border-bottom:1px dotted black;padding-bottom:15px">'.$root.'</div>';
global 
$retp;
echo 
'<div style="float:left">This site has been added to your trusted site list.</div><div style="float:right"><a href="openid.php?openid.mode=checkid_immediate'continuation().'" style="font-weight:bold">Continue</a><!-- <a href="openid.php?ui=trust">Go to Trusted Site List</a--></div></p><br clear="both" />';
}
else if ( 
$root && $_REQUEST['del_site'] ) {
if ( 
is_trusted($root) ) {
$trusted list_trusted_sites();
    
$out fopen("openid_trust","w");
foreach ( 
$trusted as $cs ) {
if ( 
trim($cs) != trim($root) ) {
    
fwrite($out,$cs."\n");
}
}
    
fclose($out);
}

echo 
'<h2>Trusted Sites</h2>';
echo 
'<div style="text-align:center;font-style:italic;margin-bottom:.7em;border-bottom:1px dotted black;padding-bottom:15px">'.$root.'</div>';
echo 
'<div style="float:left">This site has been removed from your trusted site list.</div><A HREF="openid.php?ui=trust" style="float:right">Done</A><P style="clear:both">';
}
else if ( 
$root ) {
echo 
'<h2>Trust Site</h2><form action="openid.php?'.continuation().'" method="post">';
echo 
'<fieldset>';
echo 
'<div style="text-align:center;font-style:italic;margin-bottom:.7em;border-bottom:1px dotted black;padding-bottom:15px">'.$root.'</div>';
if ( 
is_trusted($root) )
echo 
'<div style="float:left">This site is already in your trusted site list.</div></fieldset></form><br clear="both" />';
else
echo 
'<div style="float:left">This site is not in your trusted site list. Do you want to add this site to your list?</div> <input type="submit" name="add_site" value="Yes" style="float:right"></fieldset></form><br clear="both" />';
}
else if ( 
$_REQUEST['ui'] == 'trust' ) {
echo 
'<h2>Trusted Sites</h2>';
$trusted list_trusted_sites();
if ( 
count($trusted) ) {
echo 
'There are '.count($trusted).' sites on your trusted sites list.';
echo 
'<ul>';
foreach (
$trusted as $site ) {
echo 
"<li>$site (<a href=\"openid.php?openid.trust_root=".urlencode($site)."&del_site=Yes\">Remove</a>)</li>";
}
echo 
"</ul>";
}
else { echo 
"<i>You do not have any sites on your trusted sites list</i>"; }
}
else {
?>
<h2>Welcome</h2>
Welcome to your OpenID server. To manage your trusted sites, click "<a href="openid.php?ui=trust">Trust</a>" on the menu above.
<?}
   
template_footer();
   exit;
}
?>