| @ -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> | |||||