@ -1,4 +1,7 @@ | |||
<?php | |||
class Config { | |||
public static $cache = true; | |||
public static $admins = [ | |||
'https://aaronparecki.com/' | |||
]; | |||
} |
@ -1,4 +1,7 @@ | |||
<?php | |||
class Config { | |||
public static $cache = false; | |||
public static $admins = [ | |||
'https://you.example.com/' | |||
]; | |||
} |
@ -0,0 +1,156 @@ | |||
<?php | |||
use Symfony\Component\HttpFoundation\Request; | |||
use Symfony\Component\HttpFoundation\Response; | |||
class Certbot { | |||
private $mc; | |||
private $http; | |||
public function index(Request $request, Response $response) { | |||
session_start(); | |||
$state = mt_rand(10000,99999); | |||
$_SESSION['state'] = $state; | |||
$response->setContent(view('certbot', [ | |||
'title' => 'X-Ray', | |||
'state' => $state | |||
])); | |||
return $response; | |||
} | |||
public function start_auth(Request $request, Response $response) { | |||
session_start(); | |||
$_SESSION['client_id'] = $request->get('client_id'); | |||
$_SESSION['redirect_uri'] = $request->get('redirect_uri'); | |||
$query = http_build_query([ | |||
'me' => $request->get('me'), | |||
'client_id' => $request->get('client_id'), | |||
'redirect_uri' => $request->get('redirect_uri'), | |||
'state' => $request->get('state'), | |||
]); | |||
$response->headers->set('Location', 'https://indieauth.com/auth?'.$query); | |||
$response->setStatusCode(302); | |||
return $response; | |||
} | |||
public function redirect(Request $request, Response $response) { | |||
session_start(); | |||
$this->http = new p3k\HTTP(); | |||
if(!isset($_SESSION['state']) || $_SESSION['state'] != $request->get('state')) { | |||
$response->headers->set('Location', '/cert?error=invalid_state'); | |||
$response->setStatusCode(302); | |||
return $response; | |||
} | |||
if($code = $request->get('code')) { | |||
$res = $this->http->post('https://indieauth.com/auth', http_build_query([ | |||
'code' => $code, | |||
'client_id' => $_SESSION['client_id'], | |||
'redirect_uri' => $_SESSION['redirect_uri'], | |||
'state' => $_SESSION['state'] | |||
]), [ | |||
'Accept: application/json' | |||
]); | |||
$verify = json_decode($res['body'], true); | |||
unset($_SESSION['state']); | |||
if(isset($verify['me'])) { | |||
if(in_array($verify['me'], Config::$admins)) { | |||
$_SESSION['me'] = $verify['me']; | |||
$response->headers->set('Location', '/cert'); | |||
} else { | |||
$response->headers->set('Location', '/cert?error=invalid_user'); | |||
} | |||
} else { | |||
$response->headers->set('Location', '/cert?error=invalid'); | |||
} | |||
} else { | |||
$response->headers->set('Location', '/cert?error=missing_code'); | |||
} | |||
$response->setStatusCode(302); | |||
return $response; | |||
} | |||
public function save_challenge(Request $request, Response $response) { | |||
session_start(); | |||
if(!isset($_SESSION['me']) || !in_array($_SESSION['me'], Config::$admins)) { | |||
$response->headers->set('Location', '/cert?error=forbidden'); | |||
$response->setStatusCode(302); | |||
return $response; | |||
} | |||
$token = $request->get('token'); | |||
$challenge = $request->get('challenge'); | |||
if(preg_match('/acme-challenge\/(.+)/', $token, $match)) { | |||
$token = $match[1]; | |||
} elseif(!preg_match('/^[_a-zA-Z0-9]+$/', $token)) { | |||
echo "Invalid token format\n"; | |||
die(); | |||
} | |||
$this->_mc(); | |||
$this->mc->set('acme-challenge-'.$token, json_encode([ | |||
'token' => $token, | |||
'challenge' => $challenge | |||
]), 0, 600); | |||
$response->setContent(view('certbot', [ | |||
'title' => 'X-Ray', | |||
'challenge' => $challenge, | |||
'token' => $token, | |||
'verified' => true | |||
])); | |||
return $response; | |||
} | |||
public function logout(Request $request, Response $response) { | |||
session_start(); | |||
unset($_SESSION['me']); | |||
unset($_SESSION['client_id']); | |||
unset($_SESSION['redirect_uri']); | |||
unset($_SESSION['state']); | |||
session_destroy(); | |||
$response->headers->set('Location', '/cert'); | |||
$response->setStatusCode(302); | |||
return $response; | |||
} | |||
public function challenge(Request $request, Response $response, array $args) { | |||
$this->_mc(); | |||
$token = $args['token']; | |||
if($cache = $this->mc->get('acme-challenge-'.$token)) { | |||
$acme = json_decode($cache, true); | |||
$response->setContent($acme['challenge']); | |||
} else { | |||
$response->setStatusCode(404); | |||
$response->setContent("Not Found\n"); | |||
} | |||
$response->headers->set('Content-Type', 'text/plain'); | |||
return $response; | |||
} | |||
private function _mc() { | |||
$this->mc = new Memcache(); | |||
$this->mc->addServer('127.0.0.1'); | |||
} | |||
} |
@ -0,0 +1,26 @@ | |||
<?php | |||
echo "Enter a password: "; | |||
hide_term(); | |||
$password1 = trim(fgets(STDIN), PHP_EOL); | |||
echo PHP_EOL; | |||
echo "Confirm password: "; | |||
$password2 = trim(fgets(STDIN), PHP_EOL); | |||
echo PHP_EOL; | |||
restore_term(); | |||
if($password1 == $password2) { | |||
$hash = password_hash($password1, PASSWORD_DEFAULT); | |||
echo "Password hash: $hash\n"; | |||
} else { | |||
echo "Passwords did not match\n"; | |||
die(1); | |||
} | |||
function hide_term() { | |||
system('stty -echo'); | |||
} | |||
function restore_term() { | |||
system('stty echo'); | |||
} | |||
@ -0,0 +1,24 @@ | |||
<?php | |||
echo "Enter password hash to verify against: "; | |||
$hash = trim(fgets(STDIN), PHP_EOL); | |||
echo "Enter password: "; | |||
hide_term(); | |||
$password = trim(fgets(STDIN), PHP_EOL); | |||
echo PHP_EOL; | |||
restore_term(); | |||
$verified = password_verify($password, $hash); | |||
if($verified) | |||
echo "Password verified\n"; | |||
else | |||
echo "Password did not match\n"; | |||
function hide_term() { | |||
system('stty -echo'); | |||
} | |||
function restore_term() { | |||
system('stty echo'); | |||
} |
@ -0,0 +1,110 @@ | |||
<?php $this->layout('layout', ['title' => $title]); ?> | |||
<div class="column"> | |||
<h1>X-Ray Certificate Setup</h1> | |||
<?php if(isset($_SESSION['me'])): ?> | |||
<?php if(isset($verified) && $verified): ?> | |||
<div class="section"> | |||
<p>The challenge was saved and is now accessible via the <code>.well-known</code> path.</p> | |||
<p><a href="/.well-known/acme-challenge/<?= $token ?>">view challenge</a></p> | |||
</div> | |||
<?php else: ?> | |||
<div class="section"> | |||
<form class="" action="/cert/save-challenge" method="post"> | |||
<div class="field"><input type="text" name="token" placeholder="http://xray.p3k.io/.well-known/acme-challenge/_Tzyxwvut..." value="<?= isset($token) ? $token : '' ?>"></div> | |||
<div class="field"><textarea name="challenge" rows="4" placeholder="challenge value"><?= isset($challenge) ? $challenge : '' ?></textarea></div> | |||
<div class="field"><button type="submit" class="button">Save</button></div> | |||
</form> | |||
</div> | |||
<?php endif ?> | |||
<div style="margin-top: 1em; font-size: 12px;"> | |||
Signed in as <?= $_SESSION['me'] ?> <a href="/cert/logout">Sign Out</a>. | |||
</div> | |||
<?php else: ?> | |||
<div class="section"> | |||
<form class="" action="/cert/auth" method="get"> | |||
<div class="field"><input type="url" name="me" placeholder="https://you.example.com"></div> | |||
<div class="field"><button type="submit" class="button">Sign In</button></div> | |||
<input type="hidden" name="client_id" value="https://<?= $_SERVER['SERVER_NAME'] ?>/"> | |||
<input type="hidden" name="redirect_uri" value="https://<?= $_SERVER['SERVER_NAME'] ?>/cert/redirect"> | |||
<input type="hidden" name="state" value="<?= isset($state) ? $state : '' ?>"> | |||
</form> | |||
</div> | |||
<?php endif ?> | |||
</div> | |||
<script> | |||
var base = window.location.protocol + "//" + window.location.hostname + "/"; | |||
document.querySelector("input[name=client_id]").value = base; | |||
document.querySelector("input[name=redirect_uri]").value = base+"cert/redirect"; | |||
</script> | |||
<style type="text/css"> | |||
body { | |||
color: #212121; | |||
font-family: "Helvetica Neue", "Calibri Light", Roboto, sans-serif; | |||
-webkit-font-smoothing: antialiased; | |||
-moz-osx-font-smoothing: grayscale; | |||
} | |||
body { | |||
background-color: #e9e9e9; | |||
font-size: 16px; | |||
} | |||
h1 { | |||
padding-top: 6rem; | |||
padding-bottom: 1rem; | |||
text-align: center; | |||
} | |||
a { | |||
color: #4183c4; | |||
text-decoration: none; | |||
} | |||
.column { | |||
max-width: 450px; | |||
margin: 0 auto; | |||
} | |||
.section { | |||
border: 1px #ccc solid; | |||
border-radius: 6px; | |||
background: white; | |||
padding: 12px; | |||
margin-top: 2em; | |||
} | |||
.help { | |||
text-align: center; | |||
font-size: 0.9rem; | |||
} | |||
form .field { | |||
margin-bottom: .5rem; | |||
display: flex; | |||
} | |||
form input, form textarea, form button { | |||
width: 100%; | |||
border: 1px #ccc solid; | |||
border-radius: 4px; | |||
flex: 1 0; | |||
font-size: 1rem; | |||
} | |||
form input, form textarea { | |||
padding: .5rem; | |||
} | |||
form .button { | |||
background-color: #009c95; | |||
border: 0; | |||
border-radius: 4px; | |||
color: white; | |||
font-weight: bold; | |||
font-size: 1rem; | |||
cursor: pointer; | |||
padding: 0.5rem; | |||
} | |||
</style> |