friendship ended with social-app. php is my new best friend

Compare changes

Choose any two refs to compare.

Changed files
+141 -92
lib
pages
templates
vendor
chillerlan
php-oauth
src
composer
+84 -84
index.php
··· 19 19 use Matrix\Async; 20 20 use GuzzleHttp\Psr7\HttpFactory; 21 21 use chillerlan\OAuth\OAuthOptions; 22 + use chillerlan\OAuth\Storage\SessionStorage; 22 23 use DateTimeImmutable; 23 - use Lcobucci\JWT\Builder; 24 - use Lcobucci\JWT\JwtFacade; 24 + use Lcobucci\JWT\Token\Builder; 25 + use Lcobucci\JWT\Encoding\ChainedFormatter; 26 + use Lcobucci\JWT\Encoding\JoseEncoder; 27 + use Lcobucci\JWT\Signer\Key\InMemory; 25 28 use Lcobucci\JWT\Signer\Ecdsa\Sha256; 26 - use Lcobucci\JWT\Signer\Key\InMemory; 27 29 use Smallnest\Bsky\BskyProvider; 28 30 31 + session_start(); 29 32 $bskyToucher = new BskyToucher(); 30 33 31 34 $favoriteFeeds = array_map(function ($feed) use ($bskyToucher) { ··· 64 67 Flight::set('publicApi', PUBLIC_API); 65 68 Flight::set('frontpageFeed', FRONTPAGE_FEED); 66 69 Flight::set('defaultRelay', DEFAULT_RELAY); 67 - Flight::set('userAuth', null); 70 + Flight::set('userAuth', array_key_exists('sbs_'.SITE_DOMAIN, $_SESSION) ? $_SESSION['sbs_'.SITE_DOMAIN] : null); 71 + Flight::set('userPds', array_key_exists('sbs_'.SITE_DOMAIN.'_pds', $_SESSION) ? $_SESSION['sbs_'.SITE_DOMAIN.'_pds'] : null); 72 + Flight::set('userInfo', array_key_exists('sbs_'.SITE_DOMAIN.'_userinfo', $_SESSION) ? $_SESSION['sbs_'.SITE_DOMAIN.'_userinfo'] : null); 68 73 Flight::set('flight.log_errors', false); 69 74 Flight::set('flight.handle_errors', false); 70 75 Flight::set('flight.content_length', false); ··· 76 81 'setTheme' => array_key_exists('sbs_theme', $_COOKIE) ? $_COOKIE['sbs_theme'] : DEFAULT_THEME, 77 82 'setFont' => array_key_exists('sbs_font', $_COOKIE) ? $_COOKIE['sbs_font'] : DEFAULT_FONT, 78 83 'userAuth' => Flight::get('userAuth'), 84 + 'userPds' => Flight::get('userPds'), 85 + 'userInfo' => Flight::Get('userInfo'), 79 86 'favFeeds' => $favoriteFeeds, 80 87 'pages' => PAGES, 81 88 'links' => LINKS, ··· 175 182 }); 176 183 177 184 Flight::route('/login', function(): void { 178 - $options = new OAuthOptions([ 179 - 'key' => 'https://'.SITE_DOMAIN.CLIENT_ID, 180 - 'secret' => CLIENT_SECRET, 181 - 'callbackURL' => 'http://127.0.0.1/login', 182 - 'sessionStart' => true, 183 - ]); 184 - $connector = new React\Socket\Connector([ 185 - 'dns' => '1.1.1.1' 186 - ]); 187 - $http = new React\Http\Browser($connector); 188 - $httpFactory = new HttpFactory(); 189 - $client = new GuzzleHttp\Client([ 190 - 'verify' => true, 191 - 'headers' => [ 192 - 'User-Agent' => USER_AGENT_STR 193 - ] 194 - ]); 195 - $provider = new BskyProvider($options, $client, $httpFactory, $httpFactory, $httpFactory); 196 - $name = $provider->getName(); 185 + if (isset($_GET['username'])) { 197 186 $username = $_GET['username']; 198 - $bskyToucher = new BskyToucher(); 199 - $userInfo = $bskyToucher->getUserInfo($username); 200 - if (!$userInfo) die(1); 201 - $pds = $userInfo->pds; 202 - $provider->setPds($pds); 203 - $token_builder = Builder::new(new JoseEncoder(), ChainedFormatter::default()); 204 - $algorithm = new Sha256(); 205 - $signing_key = InMemory::plainText(random_bytes(32)); 206 - $now = new DateTimeImmutable(); 207 - $token = $token_builder 208 - ->withHeader('alg', 'ES256') 209 - ->withHeader('typ', 'JWT') 210 - ->withClaim('iss', $userInfo->did) 211 - ->withClaim('sub', 'https://'.SITE_DOMAIN.CLIENT_ID) 212 - ->withClaim('aud', 'did:web:'.str_replace("/", str_replace("https://", $pds))) 213 - ->withClaim('iat', strtotime('now')) 214 - ->getToken($algorithm, $signing_key); 215 - print_r($token->toString()); 216 - 217 - /*$jwt_header = base64_encode(json_encode([ 218 - 'alg' => 'ES256', 219 - 'typ' => 'JWT' 220 - ])); 221 - $jwt_body = base64_encode(json_encode([ 222 - 'iss' => $userInfo->did, 223 - 'sub' => 'https://'.SITE_DOMAIN.CLIENT_ID, 224 - 'aud' => 'did:web:'.str_replace("/", str_replace("https://", $pds)), 225 - 'jti' => hash('sha512', bin2hex(random_bytes(256 / 2))), 226 - 'iat' => strtotime('now') 227 - ])); 228 - $jwt = $jwt_header.$jwt_body.base64_encode(CERT);*/ 229 - $client->setDefaultOption('headers', [ 230 - 'User-Agent' => USER_AGENT_STR, 231 - 'Authorization' => 'Bearer: '.$token->toString() 232 - ]); 233 - if (isset($_GET['login']) && $_GET['login'] === $name) { 234 - $auth_url = $provider->getAuthorizationUrl(); 235 - header('Location: '.$auth_url); 236 - die(1); 237 - } else if (isset($_GET['code'], $_GET['state'])) { 238 - $token = $provider->getAccessToken($_GET['code'], $_GET['state']); 239 - 240 - // save the token in a permanent storage 241 - // [...] 242 - 243 - // access granted, redirect 244 - header('Location: ?granted='.$name); 245 - die(1); 246 - } else if (isset($_GET['granted']) && $_GET['granted'] === $name) { 247 - die(1); 248 - } else if (isset($_GET['error'])) { 249 - die(1); 187 + $bskyToucher = new BskyToucher(); 188 + $userInfo = $bskyToucher->getUserInfo($username); 189 + if (!$userInfo) die(1); 190 + $pds = $userInfo->pds; 191 + $options = new OAuthOptions([ 192 + 'key' => 'https://'.SITE_DOMAIN.CLIENT_ID, 193 + 'secret' => CLIENT_SECRET, 194 + 'callbackURL' => 'https://'.SITE_DOMAIN.'/login', 195 + 'sessionStart' => true, 196 + 'sessionStorageVar' => 'sbs_'.SITE_DOMAIN 197 + ]); 198 + $storage = new SessionStorage($options); 199 + $connector = new React\Socket\Connector([ 200 + 'dns' => '1.1.1.1' 201 + ]); 202 + $http = new React\Http\Browser($connector); 203 + $httpFactory = new HttpFactory(); 204 + $token_builder = Builder::new(new JoseEncoder(), ChainedFormatter::default()); 205 + $algorithm = new Sha256(); 206 + $signing_key = InMemory::file(CERT_PATH); 207 + $now = new DateTimeImmutable(); 208 + $token = $token_builder 209 + ->withHeader('alg', 'ES256') 210 + ->withHeader('typ', 'JWT') 211 + ->withHeader('kid', 'ocwgKj_O7H9at1sL6yWf9ZZ82NOM7D0xlN8HGIyWH6M') 212 + ->issuedBy('https://'.SITE_DOMAIN.CLIENT_ID) 213 + ->identifiedBy(uniqid()) 214 + ->relatedTo('https://'.SITE_DOMAIN.CLIENT_ID) 215 + ->permittedFor($pds) 216 + ->issuedAt($now->modify('-5 seconds')) 217 + ->getToken($algorithm, $signing_key); 218 + $client = new GuzzleHttp\Client([ 219 + 'verify' => true, 220 + 'headers' => [ 221 + 'User-Agent' => USER_AGENT_STR, 222 + 'Authorization' => 'Bearer: '.$token->toString() 223 + ] 224 + ]); 225 + $provider = new BskyProvider($options, $client, $httpFactory, $httpFactory, $httpFactory); 226 + $provider->setPds($pds); 227 + $name = $provider->getName(); 228 + if (isset($_GET['login']) && $_GET['login'] === $name) { 229 + $auth_url = $provider->getAuthorizationUrl([ 230 + 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 231 + 'client_assertion' => $token->toString() 232 + ]); 233 + Flight::redirect($auth_url); 234 + die(1); 235 + } else if (isset($_GET['code'], $_GET['iss'])) { 236 + $storage->storeAccessToken($_GET['code'], $name); 237 + $_SESSION['sbs_'.SITE_DOMAIN.'_pds'] = $_GET['iss']; 238 + $_SESSION['sbs_'.SITE_DOMAIN.'_userinfo'] = $bskyToucher->getUserInfo(); 239 + Flight::redirect('/'); 240 + die(1); 241 + } else if (isset($_GET['error'])) { 242 + die(1); 243 + } 244 + } else { 245 + $latte = new Latte\Engine; 246 + $latte->render('./templates/login.latte', array_merge(Flight::get('standardParams'), [ 247 + 'mainClass' => 'form', 248 + 'ogtitle' => SITE_TITLE." | login", 249 + 'ogdesc' => SITE_DESC, 250 + 'ogimage' => '', 251 + 'ogurl' => 'https://'.SITE_DOMAIN.'/login' 252 + ])); 250 253 } 251 - $latte = new Latte\Engine; 252 - $latte->render('./templates/login.latte', array_merge(Flight::get('standardParams'), [ 253 - 'mainClass' => 'form', 254 - 'ogtitle' => SITE_TITLE." | login", 255 - 'ogdesc' => SITE_DESC, 256 - 'ogimage' => '', 257 - 'ogurl' => 'https://'.SITE_DOMAIN.'/login' 258 - ])); 259 254 }); 260 255 261 - // https://shimaenaga.veryroundbird.house/oauth/authorize?client_id=https%3A%2F%2Ftangled.org%2Foauth%2Fclient-metadata.json&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Areq-2399ff42af66498132ebf8de809254b7 256 + Flight::route('/logout', function(): void { 257 + unset($_SESSION['sbs_'.SITE_DOMAIN]); 258 + unset($_SESSION['sbs_'.SITE_DOMAIN.'_pds']); 259 + unset($_SESSION['sbs_'.SITE_DOMAIN.'_userinfo']); 260 + Flight::redirect('/'); 261 + }); 262 262 263 263 Flight::route('/createaccount', function(): void { 264 264 $latte = new Latte\Engine;
+40 -2
lib/bskyProvider.php
··· 6 6 7 7 use chillerlan\OAuth\Core\OAuth2Interface; 8 8 use chillerlan\OAuth\Core\OAuth2Provider; 9 - use chillerlan\OAuth\Core\PARTrait; 10 9 use chillerlan\OAuth\Core\PKCETrait; 11 10 use chillerlan\OAuth\OAuthOptions; 12 11 use chillerlan\OAuth\Storage\SessionStorage; 12 + use chillerlan\HTTP\Utils\MessageUtil; 13 + use chillerlan\HTTP\Utils\QueryUtil; 14 + use chillerlan\OAuth\Providers\ProviderException; 13 15 use GuzzleHttp\Client; 14 16 use GuzzleHttp\Psr7\HttpFactory; 17 + use Psr\Http\Message\UriInterface; 18 + use function sprintf; 15 19 16 20 class BskyProvider extends OAuth2Provider implements \chillerlan\OAuth\Core\PAR, \chillerlan\OAuth\Core\PKCE { 17 - use \chillerlan\OAuth\Core\PARTrait; 18 21 use \chillerlan\OAuth\Core\PKCETrait; 19 22 20 23 public const IDENTIFIER = 'BSKYPROVIDER'; ··· 52 55 $this->parAuthorizationURL = (string)$pds->withPath('/oauth/par'); 53 56 54 57 return $this; 58 + } 59 + 60 + public function getParRequestUri(array $body):UriInterface{ 61 + // send the request with the same method and parameters as the token requests 62 + // @link https://datatracker.ietf.org/doc/html/rfc9126#name-request 63 + $response = $this->sendAccessTokenRequest($this->parAuthorizationURL, $body); 64 + $status = $response->getStatusCode(); 65 + $json = MessageUtil::decodeJSON($response, true); 66 + 67 + // something went horribly wrong 68 + if($status !== 201){ 69 + 70 + // @link https://datatracker.ietf.org/doc/html/rfc9126#section-2.3 71 + if(isset($json['error'], $json['error_description'])){ 72 + throw new ProviderException(sprintf('PAR error: "%s" (%s)', $json['error'], $json['error_description'])); 73 + } 74 + 75 + throw new ProviderException(sprintf('PAR request error: (HTTP/%s)', $status)); // @codeCoverageIgnore 76 + } 77 + 78 + $url = QueryUtil::merge($this->authorizationURL, $this->getParAuthorizationURLRequestParams($json)); 79 + 80 + return $this->uriFactory->createUri($url); 81 + } 82 + 83 + protected function getParAuthorizationURLRequestParams(array $response):array{ 84 + 85 + if(!isset($response['request_uri'])){ 86 + throw new ProviderException('PAR response error: "request_uri" missing'); 87 + } 88 + 89 + return [ 90 + 'client_id' => $this->options->key, 91 + 'request_uri' => $response['request_uri'], 92 + ]; 55 93 } 56 94 } 57 95 ?>
+1
lib/bskyToucher.php
··· 25 25 use Psr\Http\Message\RequestInterface; 26 26 use Psr\Http\Message\ResponseInterface; 27 27 use Fusonic\OpenGraph\Consumer; 28 + use chillerlan\OAuth\Storage\SessionStorage; 28 29 29 30 class BskyToucher { 30 31 private $bskyApiBase = 'https://public.api.bsky.app/xrpc/';
+2
pages/privacy.md
··· 2 2 3 3 smallbird social is meant to run as lightweight as possible and stores no data on its own server. if you are using a veryroundbird.house pds, your data and activity is on *that* server, but it only stores what's necessary to, you know, interact with the atproto ecosystem. so, user ID information, your posts, follows, likes, etc. 4 4 5 + the server also caches publicly-available post and user data to speed up requests. none of this data is kept longer than, like, a few days unless it's requested again. 6 + 5 7 there is some minimal tracking via goatcounter, but your data will never touch advertisers and it doesn't track individual users; i mostly just want to know what the site usage numbers are and where people found it from. 6 8 7 9 i have never been asked to or required to turn over data to any law enforcement agency.
+5
pages/terms.md
··· 1 + ## terms of use 2 + 3 + i reserve the right to block any IPs that are hitting the site too hard. i also reserve the right to end service at any time if for some reason it gets too hard to maintain or something like that. some features may be jank, broken, or missing due to the limitations of my approach and time. 4 + 5 + just be normal, ok. use the site like a normal person. that's what it's for.
+2 -1
templates/_partials/nav.latte
··· 1 1 <nav> 2 2 <ul> 3 3 {if $userAuth} 4 + <li><a href="/u/{$userInfo->handle}">profile</a></li> 4 5 <li><a href="/settings">settings</a></li> 5 - <li><a href="#">log out</a></li> 6 + <li><a href="/logout">log out</a></li> 6 7 {else} 7 8 <li><a href="/createaccount">create</a></li> 8 9 <li><a href="/login">log in</a></li>
+4
templates/layout.latte
··· 24 24 data-theme="{$setTheme}" 25 25 data-font="{$setFont}" 26 26 > 27 + <!-- 28 + {print_r($_SESSION)} 29 + {print_r(PHP_SESSION_DISABLED)} 30 + --> 27 31 <div id="page"> 28 32 <header> 29 33 <h1><a href="/">{include '_partials/logo.latte'}{$siteTitle}</a></h1>
+1 -3
vendor/chillerlan/php-oauth/src/Core/PARTrait.php
··· 35 35 public function getParRequestUri(array $body):UriInterface{ 36 36 // send the request with the same method and parameters as the token requests 37 37 // @link https://datatracker.ietf.org/doc/html/rfc9126#name-request 38 - print_r($this->parAuthorizationURL); 39 38 $response = $this->sendAccessTokenRequest($this->parAuthorizationURL, $body); 40 39 $status = $response->getStatusCode(); 41 40 $json = MessageUtil::decodeJSON($response, true); 42 - print_r($body); 43 - print_r($json); 44 41 45 42 // something went horribly wrong 46 43 if($status !== 200){ 44 + print_r($json); 47 45 48 46 // @link https://datatracker.ietf.org/doc/html/rfc9126#section-2.3 49 47 if(isset($json['error'], $json['error_description'])){
+2 -2
vendor/composer/installed.php
··· 3 3 'name' => '__root__', 4 4 'pretty_version' => 'dev-main', 5 5 'version' => 'dev-main', 6 - 'reference' => '988c87ce046f0ee346497af8fb5c755d3ae75b7b', 6 + 'reference' => '1ea5f55bde378131812f28e47f7d5b6558b04018', 7 7 'type' => 'library', 8 8 'install_path' => __DIR__ . '/../../', 9 9 'aliases' => array(), ··· 13 13 '__root__' => array( 14 14 'pretty_version' => 'dev-main', 15 15 'version' => 'dev-main', 16 - 'reference' => '988c87ce046f0ee346497af8fb5c755d3ae75b7b', 16 + 'reference' => '1ea5f55bde378131812f28e47f7d5b6558b04018', 17 17 'type' => 'library', 18 18 'install_path' => __DIR__ . '/../../', 19 19 'aliases' => array(),