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

Compare changes

Choose any two refs to compare.

Changed files
+135 -88
lib
pages
templates
vendor
chillerlan
php-oauth
src
+80 -82
index.php
···
use Matrix\Async;
use GuzzleHttp\Psr7\HttpFactory;
use chillerlan\OAuth\OAuthOptions;
+
use chillerlan\OAuth\Storage\SessionStorage;
use DateTimeImmutable;
use Lcobucci\JWT\Token\Builder;
use Lcobucci\JWT\Encoding\ChainedFormatter;
···
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Smallnest\Bsky\BskyProvider;
+
session_start();
$bskyToucher = new BskyToucher();
$favoriteFeeds = array_map(function ($feed) use ($bskyToucher) {
···
Flight::set('publicApi', PUBLIC_API);
Flight::set('frontpageFeed', FRONTPAGE_FEED);
Flight::set('defaultRelay', DEFAULT_RELAY);
-
Flight::set('userAuth', null);
+
Flight::set('userAuth', array_key_exists('sbs_'.SITE_DOMAIN, $_SESSION) ? $_SESSION['sbs_'.SITE_DOMAIN] : null);
+
Flight::set('userPds', array_key_exists('sbs_'.SITE_DOMAIN.'_pds', $_SESSION) ? $_SESSION['sbs_'.SITE_DOMAIN.'_pds'] : null);
+
Flight::set('userInfo', array_key_exists('sbs_'.SITE_DOMAIN.'_userinfo', $_SESSION) ? $_SESSION['sbs_'.SITE_DOMAIN.'_userinfo'] : null);
Flight::set('flight.log_errors', false);
Flight::set('flight.handle_errors', false);
Flight::set('flight.content_length', false);
···
'setTheme' => array_key_exists('sbs_theme', $_COOKIE) ? $_COOKIE['sbs_theme'] : DEFAULT_THEME,
'setFont' => array_key_exists('sbs_font', $_COOKIE) ? $_COOKIE['sbs_font'] : DEFAULT_FONT,
'userAuth' => Flight::get('userAuth'),
+
'userPds' => Flight::get('userPds'),
+
'userInfo' => Flight::Get('userInfo'),
'favFeeds' => $favoriteFeeds,
'pages' => PAGES,
'links' => LINKS,
···
});
Flight::route('/login', function(): void {
-
$options = new OAuthOptions([
-
'key' => 'https://'.SITE_DOMAIN.CLIENT_ID,
-
'secret' => CLIENT_SECRET,
-
'callbackURL' => 'http://127.0.0.1/login',
-
'sessionStart' => true,
-
]);
-
$connector = new React\Socket\Connector([
-
'dns' => '1.1.1.1'
-
]);
-
$http = new React\Http\Browser($connector);
-
$httpFactory = new HttpFactory();
-
$client = new GuzzleHttp\Client([
-
'verify' => true,
-
'headers' => [
-
'User-Agent' => USER_AGENT_STR
-
]
-
]);
-
$provider = new BskyProvider($options, $client, $httpFactory, $httpFactory, $httpFactory);
-
$name = $provider->getName();
+
if (isset($_GET['username'])) {
$username = $_GET['username'];
-
$bskyToucher = new BskyToucher();
-
$userInfo = $bskyToucher->getUserInfo($username);
-
if (!$userInfo) die(1);
-
$pds = $userInfo->pds;
-
$provider->setPds($pds);
-
$token_builder = Builder::new(new JoseEncoder(), ChainedFormatter::default());
-
$algorithm = new Sha256();
-
$signing_key = InMemory::plainText(CLIENT_SECRET);
-
print_r($signing_key);
-
$now = new DateTimeImmutable();
-
$token = $token_builder
-
->withHeader('alg', 'ES256')
-
->withHeader('typ', 'JWT')
-
->issuedBy($userInfo->did)
-
->relatedTo('https://'.SITE_DOMAIN.CLIENT_ID)
-
->permittedFor('did:web:'.str_replace("/", "", str_replace("https://", "", $pds)))
-
->issuedAt($now)
-
->getToken($algorithm, $signing_key);
-
print_r($token->toString());
-
-
/*$jwt_header = base64_encode(json_encode([
-
'alg' => 'ES256',
-
'typ' => 'JWT'
-
]));
-
$jwt_body = base64_encode(json_encode([
-
'iss' => $userInfo->did,
-
'sub' => 'https://'.SITE_DOMAIN.CLIENT_ID,
-
'aud' => 'did:web:'.str_replace("/", str_replace("https://", $pds)),
-
'jti' => hash('sha512', bin2hex(random_bytes(256 / 2))),
-
'iat' => strtotime('now')
-
]));
-
$jwt = $jwt_header.$jwt_body.base64_encode(CERT);*/
-
$client->setDefaultOption('headers', [
-
'User-Agent' => USER_AGENT_STR,
-
'Authorization' => 'Bearer: '.$token->toString()
-
]);
-
if (isset($_GET['login']) && $_GET['login'] === $name) {
-
$auth_url = $provider->getAuthorizationUrl();
-
header('Location: '.$auth_url);
-
die(1);
-
} else if (isset($_GET['code'], $_GET['state'])) {
-
$token = $provider->getAccessToken($_GET['code'], $_GET['state']);
-
-
// save the token in a permanent storage
-
// [...]
-
-
// access granted, redirect
-
header('Location: ?granted='.$name);
-
die(1);
-
} else if (isset($_GET['granted']) && $_GET['granted'] === $name) {
-
die(1);
-
} else if (isset($_GET['error'])) {
-
die(1);
+
$bskyToucher = new BskyToucher();
+
$userInfo = $bskyToucher->getUserInfo($username);
+
if (!$userInfo) die(1);
+
$pds = $userInfo->pds;
+
$options = new OAuthOptions([
+
'key' => 'https://'.SITE_DOMAIN.CLIENT_ID,
+
'secret' => CLIENT_SECRET,
+
'callbackURL' => 'https://'.SITE_DOMAIN.'/login',
+
'sessionStart' => true,
+
'sessionStorageVar' => 'sbs_'.SITE_DOMAIN
+
]);
+
$storage = new SessionStorage($options);
+
$connector = new React\Socket\Connector([
+
'dns' => '1.1.1.1'
+
]);
+
$http = new React\Http\Browser($connector);
+
$httpFactory = new HttpFactory();
+
$token_builder = Builder::new(new JoseEncoder(), ChainedFormatter::default());
+
$algorithm = new Sha256();
+
$signing_key = InMemory::file(CERT_PATH);
+
$now = new DateTimeImmutable();
+
$token = $token_builder
+
->withHeader('alg', 'ES256')
+
->withHeader('typ', 'JWT')
+
->withHeader('kid', 'ocwgKj_O7H9at1sL6yWf9ZZ82NOM7D0xlN8HGIyWH6M')
+
->issuedBy('https://'.SITE_DOMAIN.CLIENT_ID)
+
->identifiedBy(uniqid())
+
->relatedTo('https://'.SITE_DOMAIN.CLIENT_ID)
+
->permittedFor($pds)
+
->issuedAt($now->modify('-5 seconds'))
+
->getToken($algorithm, $signing_key);
+
$client = new GuzzleHttp\Client([
+
'verify' => true,
+
'headers' => [
+
'User-Agent' => USER_AGENT_STR,
+
'Authorization' => 'Bearer: '.$token->toString()
+
]
+
]);
+
$provider = new BskyProvider($options, $client, $httpFactory, $httpFactory, $httpFactory);
+
$provider->setPds($pds);
+
$name = $provider->getName();
+
if (isset($_GET['login']) && $_GET['login'] === $name) {
+
$auth_url = $provider->getAuthorizationUrl([
+
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
+
'client_assertion' => $token->toString()
+
]);
+
Flight::redirect($auth_url);
+
die(1);
+
} else if (isset($_GET['code'], $_GET['iss'])) {
+
$storage->storeAccessToken($_GET['code'], $name);
+
$_SESSION['sbs_'.SITE_DOMAIN.'_pds'] = $_GET['iss'];
+
$_SESSION['sbs_'.SITE_DOMAIN.'_userinfo'] = $bskyToucher->getUserInfo();
+
Flight::redirect('/');
+
die(1);
+
} else if (isset($_GET['error'])) {
+
die(1);
+
}
+
} else {
+
$latte = new Latte\Engine;
+
$latte->render('./templates/login.latte', array_merge(Flight::get('standardParams'), [
+
'mainClass' => 'form',
+
'ogtitle' => SITE_TITLE." | login",
+
'ogdesc' => SITE_DESC,
+
'ogimage' => '',
+
'ogurl' => 'https://'.SITE_DOMAIN.'/login'
+
]));
}
-
$latte = new Latte\Engine;
-
$latte->render('./templates/login.latte', array_merge(Flight::get('standardParams'), [
-
'mainClass' => 'form',
-
'ogtitle' => SITE_TITLE." | login",
-
'ogdesc' => SITE_DESC,
-
'ogimage' => '',
-
'ogurl' => 'https://'.SITE_DOMAIN.'/login'
-
]));
});
-
// 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
+
Flight::route('/logout', function(): void {
+
unset($_SESSION['sbs_'.SITE_DOMAIN]);
+
unset($_SESSION['sbs_'.SITE_DOMAIN.'_pds']);
+
unset($_SESSION['sbs_'.SITE_DOMAIN.'_userinfo']);
+
Flight::redirect('/');
+
});
Flight::route('/createaccount', function(): void {
$latte = new Latte\Engine;
+40 -2
lib/bskyProvider.php
···
use chillerlan\OAuth\Core\OAuth2Interface;
use chillerlan\OAuth\Core\OAuth2Provider;
-
use chillerlan\OAuth\Core\PARTrait;
use chillerlan\OAuth\Core\PKCETrait;
use chillerlan\OAuth\OAuthOptions;
use chillerlan\OAuth\Storage\SessionStorage;
+
use chillerlan\HTTP\Utils\MessageUtil;
+
use chillerlan\HTTP\Utils\QueryUtil;
+
use chillerlan\OAuth\Providers\ProviderException;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
+
use Psr\Http\Message\UriInterface;
+
use function sprintf;
class BskyProvider extends OAuth2Provider implements \chillerlan\OAuth\Core\PAR, \chillerlan\OAuth\Core\PKCE {
-
use \chillerlan\OAuth\Core\PARTrait;
use \chillerlan\OAuth\Core\PKCETrait;
public const IDENTIFIER = 'BSKYPROVIDER';
···
$this->parAuthorizationURL = (string)$pds->withPath('/oauth/par');
return $this;
+
}
+
+
public function getParRequestUri(array $body):UriInterface{
+
// send the request with the same method and parameters as the token requests
+
// @link https://datatracker.ietf.org/doc/html/rfc9126#name-request
+
$response = $this->sendAccessTokenRequest($this->parAuthorizationURL, $body);
+
$status = $response->getStatusCode();
+
$json = MessageUtil::decodeJSON($response, true);
+
+
// something went horribly wrong
+
if($status !== 201){
+
+
// @link https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
+
if(isset($json['error'], $json['error_description'])){
+
throw new ProviderException(sprintf('PAR error: "%s" (%s)', $json['error'], $json['error_description']));
+
}
+
+
throw new ProviderException(sprintf('PAR request error: (HTTP/%s)', $status)); // @codeCoverageIgnore
+
}
+
+
$url = QueryUtil::merge($this->authorizationURL, $this->getParAuthorizationURLRequestParams($json));
+
+
return $this->uriFactory->createUri($url);
+
}
+
+
protected function getParAuthorizationURLRequestParams(array $response):array{
+
+
if(!isset($response['request_uri'])){
+
throw new ProviderException('PAR response error: "request_uri" missing');
+
}
+
+
return [
+
'client_id' => $this->options->key,
+
'request_uri' => $response['request_uri'],
+
];
}
}
?>
+1
lib/bskyToucher.php
···
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Fusonic\OpenGraph\Consumer;
+
use chillerlan\OAuth\Storage\SessionStorage;
class BskyToucher {
private $bskyApiBase = 'https://public.api.bsky.app/xrpc/';
+2
pages/privacy.md
···
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.
+
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.
+
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.
i have never been asked to or required to turn over data to any law enforcement agency.
+5
pages/terms.md
···
+
## terms of use
+
+
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.
+
+
just be normal, ok. use the site like a normal person. that's what it's for.
+2 -1
templates/_partials/nav.latte
···
<nav>
<ul>
{if $userAuth}
+
<li><a href="/u/{$userInfo->handle}">profile</a></li>
<li><a href="/settings">settings</a></li>
-
<li><a href="#">log out</a></li>
+
<li><a href="/logout">log out</a></li>
{else}
<li><a href="/createaccount">create</a></li>
<li><a href="/login">log in</a></li>
+4
templates/layout.latte
···
data-theme="{$setTheme}"
data-font="{$setFont}"
>
+
<!--
+
{print_r($_SESSION)}
+
{print_r(PHP_SESSION_DISABLED)}
+
-->
<div id="page">
<header>
<h1><a href="/">{include '_partials/logo.latte'}{$siteTitle}</a></h1>
+1 -3
vendor/chillerlan/php-oauth/src/Core/PARTrait.php
···
public function getParRequestUri(array $body):UriInterface{
// send the request with the same method and parameters as the token requests
// @link https://datatracker.ietf.org/doc/html/rfc9126#name-request
-
print_r($this->parAuthorizationURL);
$response = $this->sendAccessTokenRequest($this->parAuthorizationURL, $body);
$status = $response->getStatusCode();
$json = MessageUtil::decodeJSON($response, true);
-
print_r($body);
-
print_r($json);
// something went horribly wrong
if($status !== 200){
+
print_r($json);
// @link https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
if(isset($json['error'], $json['error_description'])){