Skip to content

Commit

Permalink
Fixes #22 -- Implement VRC API directly in PHP for future VRC API calls.
Browse files Browse the repository at this point in the history
  • Loading branch information
BusterNeece committed Apr 11, 2024
1 parent 83dc9a2 commit ee60052
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 103 deletions.
3 changes: 0 additions & 3 deletions backend/config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,6 @@
$group->get('/json', App\Controller\Api\JsonAction::class)
->setName('api:json');

$group->post('/vrc_api', App\Controller\Api\VrcApiAction::class)
->setName('api:vrc_api');

$group->group('/comments', function (RouteCollectorProxy $group) {
$group->get('/{location}', App\Controller\Api\CommentsController::class . ':listAction')
->setName('api:comments');
Expand Down
26 changes: 21 additions & 5 deletions backend/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Psr\Cache\CacheItemPoolInterface;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel as PsrLogLevel;
use Psr\SimpleCache\CacheInterface;
use Slim\Factory\ServerRequestCreatorFactory;
use Slim\Handlers\Strategies\RequestResponse;
Expand Down Expand Up @@ -132,11 +133,26 @@
},

// HTTP client
HttpClient::class => static fn() => new HttpClient([
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
],
]),
HttpClient::class => static function (
Logger $logger
) {
$stack = GuzzleHttp\HandlerStack::create();

$stack->push(
GuzzleHttp\Middleware::log(
$logger,
new GuzzleHttp\MessageFormatter('HTTP client {method} call to {uri} produced response {code}'),
PsrLogLevel::DEBUG
)
);

return new HttpClient([
'handler' => $stack,
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
],
]);
},

// Filesystem Utilities
Filesystem::class => static fn() => new Filesystem(),
Expand Down
40 changes: 0 additions & 40 deletions backend/src/Controller/Api/VrcApiAction.php

This file was deleted.

86 changes: 34 additions & 52 deletions backend/src/Service/VrcApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,49 @@

namespace App\Service;

use App\Environment;
use App\Service\VrcApi\AuthMiddleware;
use App\Service\VrcApi\RateLimitStore;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Middleware;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Spatie\GuzzleRateLimiterMiddleware\RateLimiterMiddleware;

final readonly class VrcApi
{
public const string VRCAPI_BASE_URL = 'https://api.vrchat.cloud/api/1/';

private Client $httpClient;

public function __construct(
private Client $http
LoggerInterface $logger,
AuthMiddleware $authMiddleware,
RateLimitStore $rateLimitStore
) {
}

/**
* Send a request to the API proxy server
*
* @param string $method The HTTP method to perform (POST, GET, PUT, etc)
* @param string $path The path to use (/api/1/visits etc)
* @param string|null $body The HTTP body (if any, set to null if not wanted)
* @param bool $priority If the request should use the high-priority queue
* @param bool $async If the request should be async (non-failable, ideal for friend requests/invite requests)
* @return string|array
*/
public function sendRequest(
string $method,
string $path,
?string $body = null,
bool $priority = false,
bool $async = false
): string|array {
/** @noinspection HttpUrlsUsage */
$uri = 'http://149.106.100.136:8124/' . $path;
$stack = HandlerStack::create();
$stack->push(
Middleware::log(
$logger,
new MessageFormatter('VRCAPI client {method} call to {uri} produced response {code}'),
LogLevel::DEBUG
)
);
$stack->push(RateLimiterMiddleware::perSecond(1, $rateLimitStore));
$stack->push($authMiddleware);

$requestConfig = [
$this->httpClient = new Client([
'handler' => $stack,
'base_uri' => self::VRCAPI_BASE_URL,
'headers' => [
'Authorization' => $_ENV['VRCHAT_API_KEY'],
'User-Agent' => 'WaterWolf/1.0 [email protected]',
],
];

if ($priority) {
$requestConfig['headers']['X-Priority'] = 'high';
}
if ($async) {
$requestConfig['headers']['X-Background'] = '1';
}

if ($body !== null) {
$requestConfig['json'] = $body;
}

$response = $this->http->request(
$method,
$uri,
$requestConfig
);
]);
}

if ($response->getHeaderLine('Content-Type') === 'application/json') {
return json_decode(
$response->getBody()->getContents(),
true,
JSON_THROW_ON_ERROR
);
} else {
return $response->getBody()->getContents();
}
public function getHttpClient(): Client
{
return $this->httpClient;
}
}
108 changes: 108 additions & 0 deletions backend/src/Service/VrcApi/AuthMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace App\Service\VrcApi;

use App\Environment;
use App\Service\VrcApi;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Cookie\FileCookieJar;
use GuzzleHttp\Cookie\SetCookie;
use OTPHP\TOTP;
use Psr\Http\Message\RequestInterface;

final readonly class AuthMiddleware
{
public const string VRC_AUTH_COOKIE_NAME = 'auth';
public const string VRC_TOTP_COOKIE_NAME = 'twoFactorAuth';

private string|null $username;
private string|null $password;
private string|null $totp;

private CookieJar $cookieJar;

public function __construct(
private Client $httpClient
) {
$this->username = $_ENV['VRCHAT_USERNAME'] ?? null;
$this->password = $_ENV['VRCHAT_PASSWORD'] ?? null;
$this->totp = $_ENV['VRCHAT_TOTP'] ?? null;

$this->cookieJar = new FileCookieJar(Environment::getTempDirectory() . '/vrcapi_cookies');
}

public function __invoke(callable $next): \Closure
{
return function (RequestInterface $request, array $options = []) use (&$next) {
$request = $this->applyAuth($request);
return $next($request, $options);
};
}

protected function applyAuth(RequestInterface $request): RequestInterface
{
if (!$this->hasValidCookies()) {
$this->reAuth();
}

return $this->cookieJar->withCookieHeader($request);
}

private function reAuth(): void
{
$options = [
'base_uri' => VrcApi::VRCAPI_BASE_URL,
'cookies' => $this->cookieJar,
'headers' => [
'Authorization' => 'Basic ' . base64_encode(
urlencode($this->username) . ':' . urlencode($this->password)
),
],
];

if (!empty($this->totp)) {
$totp = TOTP::createFromSecret(str_replace(' ', '', strtoupper($this->totp)));
$this->httpClient->post(
'auth/twofactorauth/totp/verify',
[
...$options,
'json' => [
'code' => $totp->now(),
],
]
);
} else {
$this->httpClient->get(
'auth/user',
$options
);
}
}

private function hasValidCookies(): bool
{
if ($this->cookieJar->count() === 0) {
return false;
}

$cookieNames = [];

/** @var SetCookie $cookie */
foreach ($this->cookieJar->getIterator() as $cookie) {
if (!$cookie->isExpired()) {
$cookieNames[$cookie->getName()] = $cookie;
}
}

if (!isset($cookieNames[self::VRC_AUTH_COOKIE_NAME])) {
return false;
}

if (!empty($this->totp) && !isset($cookieNames[self::VRC_TOTP_COOKIE_NAME])) {
return false;
}

return true;
}
}
29 changes: 29 additions & 0 deletions backend/src/Service/VrcApi/RateLimitStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace App\Service\VrcApi;

use Psr\SimpleCache\CacheInterface;
use Spatie\GuzzleRateLimiterMiddleware\Store;

final readonly class RateLimitStore implements Store
{
public const string CACHE_KEY = 'vrcapi_rate_limit';

public function __construct(
private CacheInterface $psrCache
) {
}

public function get(): array
{
return $this->psrCache->get(self::CACHE_KEY, []);
}

public function push(int $timestamp, int $limit): void
{
$entries = $this->get();
$entries[] = $timestamp;

$this->psrCache->set(self::CACHE_KEY, $entries, $limit);
}
}
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"robmorgan/phinx": "^0.16.0",
"slim/http": "^1.3",
"slim/slim": "^4.12",
"spatie/guzzle-rate-limiter-middleware": "^2.0",
"spomky-labs/otphp": "^11.2",
"symfony/amazon-mailer": "^7.0",
"symfony/cache": "^7.0",
"symfony/console": "^7.0",
Expand Down
Loading

0 comments on commit ee60052

Please sign in to comment.