+84
-84
index.php
+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
+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
+1
lib/bskyToucher.php
+2
pages/privacy.md
+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
+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.
+4
templates/layout.latte
+4
templates/layout.latte
+1
-3
vendor/chillerlan/php-oauth/src/Core/PARTrait.php
+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
+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(),