@ -1,4 +1,7 @@ | |||||
<?php | <?php | ||||
class Config { | class Config { | ||||
public static $cache = true; | public static $cache = true; | ||||
public static $admins = [ | |||||
'https://aaronparecki.com/' | |||||
]; | |||||
} | } |
@ -1,4 +1,7 @@ | |||||
<?php | <?php | ||||
class Config { | class Config { | ||||
public static $cache = false; | 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> |