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