Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,18 @@ Verifier-Server is a web server designed to handle BYOND user verification.
#DB_OPTIONS=

# MySQL configuration
DB_DSN=mysql:host=127.0.0.1;port=3306;dbname=verify_list
DB_PORT=3306
DB_USERNAME=your_username
DB_PASSWORD=your_password
#DB_DSN=mysql:host=127.0.0.1;port=3306;dbname=verify_list
#DB_PORT=3306
#DB_USERNAME=your_username
#DB_PASSWORD=your_password
#DB_OPTIONS={"option1":"value1","option2":"value2"}
```

The `.env` file lives in the repository root regardless of the working
directory; variables set in the real environment take precedence over it.
The OAuth2 web endpoints (`SS14_OAUTH2_CLIENT_ID`/`SECRET`, `dwa_client_id`/
`dwa_client_secret`) are optional and only registered when both halves are set.

## Usage

1. Start the server:
Expand Down
15 changes: 11 additions & 4 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ TOKEN=changeme
STORAGE_TYPE=filesystem
#CWD-relative path to the JSON file
JSON_PATH=json/verify.json
SS14_JSON_PATH=json/ss14verify.json

# SQLite configuration
#DB_DSN=sqlite:verify.db
Expand All @@ -12,8 +13,14 @@ JSON_PATH=json/verify.json
#DB_OPTIONS=

# MySQL configuration
DB_DSN=mysql:host=127.0.0.1;port=3306;dbname=verify_list
DB_PORT=3306
DB_USERNAME=your_username
DB_PASSWORD=your_password
#DB_DSN=mysql:host=127.0.0.1;port=3306;dbname=verify_list
#DB_PORT=3306
#DB_USERNAME=your_username
#DB_PASSWORD=your_password
#DB_OPTIONS={"option1":"value1","option2":"value2"}

# Optional OAuth2 web endpoints (/ss14wa and /dwa); left unset, they are not registered.
#SS14_OAUTH2_CLIENT_ID=
#SS14_OAUTH2_CLIENT_SECRET=
#dwa_client_id=
#dwa_client_secret=
16 changes: 8 additions & 8 deletions run.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
$envConfig['STORAGE_TYPE'] ?? 'filesystem',
$envConfig['SS14_JSON_PATH'] ?? 'json/ss14verify.json',
]);
$server->setSS14OAuth2Endpoint(
$_ENV['SS14_OAUTH2_CLIENT_ID'] ?? getenv('SS14_OAUTH2_CLIENT_ID'),
$_ENV['SS14_OAUTH2_CLIENT_SECRET'] ?? getenv('SS14_OAUTH2_CLIENT_SECRET')
);
$server->setDiscordOAuth2Endpoint(
$_ENV['dwa_client_id'] ?? getenv('dwa_client_id'),
$_ENV['dwa_client_secret'] ?? getenv('dwa_client_secret')
);
// OAuth2 web endpoints are optional; they are only registered when both credentials are configured.
$oauth = static fn(string $key): string => (string) ($_ENV[$key] ?? getenv($key) ?: '');
if ($oauth('SS14_OAUTH2_CLIENT_ID') !== '' && $oauth('SS14_OAUTH2_CLIENT_SECRET') !== '') {
$server->setSS14OAuth2Endpoint($oauth('SS14_OAUTH2_CLIENT_ID'), $oauth('SS14_OAUTH2_CLIENT_SECRET'));
}
if ($oauth('dwa_client_id') !== '' && $oauth('dwa_client_secret') !== '') {
$server->setDiscordOAuth2Endpoint($oauth('dwa_client_id'), $oauth('dwa_client_secret'));
}
$server->start(true); // Start the server and the event loop
2 changes: 1 addition & 1 deletion src/VerifierServer/OAuth2Authenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public function __construct(

$this->state = isset($this->sessions[$this->endpoint_name][$this->requesting_ip], $this->sessions[$this->endpoint_name][$this->requesting_ip]['state'])
? $this->sessions[$this->endpoint_name][$this->requesting_ip]['state']
: $this->sessions[$this->endpoint_name][$this->requesting_ip]['state'] = uniqid();
: $this->sessions[$this->endpoint_name][$this->requesting_ip]['state'] = bin2hex(random_bytes(16)); // unguessable CSRF state

if (isset($this->sessions[$this->endpoint_name][$this->requesting_ip]['access_token'])) {
$this->access_token = $this->sessions[$this->endpoint_name][$this->requesting_ip]['access_token'];
Expand Down
93 changes: 53 additions & 40 deletions src/VerifierServer/PersistentState.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,57 +175,70 @@ public function getJsonPath(): string
return getcwd() . '/' . $this->json_path;
}

/** Keys returned by loadEnvConfig() with their defaults when absent from the environment. */
public const ENV_DEFAULTS = [
'HOST_ADDR' => '127.0.0.1',
'HOST_PORT' => '8080',
'TOKEN' => 'changeme',
'STORAGE_TYPE' => 'filesystem',
'JSON_PATH' => 'json/verify.json',
'SS14_JSON_PATH' => 'json/ss14verify.json',
'DB_DSN' => '',
'DB_PORT' => '',
'DB_USERNAME' => '',
'DB_PASSWORD' => '',
'DB_OPTIONS' => '',
];

/**
* Loads environment configuration from a .env file.
* If the .env file does not exist, it creates one with default values.
*
* @return array The environment configuration as an associative array.
* Loads environment configuration from the `.env` file in `$directory`
* (the repository root by default). If the file does not exist, it is
* created with default values. Variables already present in the real
* environment take precedence over the file.
*
* @return array The environment configuration as an associative array
* with every key of ENV_DEFAULTS present.
* @throws Exception If the TOKEN is 'changeme' and the HOST_ADDR is not '127.0.0.1'.
*
* After loading the .env file, it checks if the TOKEN is 'changeme' and the HOST_ADDR is not '127.0.0.1'.
* If this condition is met, the script terminates with an error message.
*
*/
public static function loadEnvConfig(): array
public static function loadEnvConfig(?string $directory = null): array
{
if (! file_exists(".env")) {
file_put_contents(".env",
"HOST_ADDR=127.0.0.1" . PHP_EOL .
"HOST_PORT=8080" . PHP_EOL .
"TOKEN=changeme" . PHP_EOL .
"STORAGE_TYPE=filesystem" . PHP_EOL .
$directory = $directory ?? dirname(__DIR__, 2);
$file = rtrim($directory, '/\\') . DIRECTORY_SEPARATOR . '.env';
if (! file_exists($file)) {
file_put_contents($file,
"HOST_ADDR=127.0.0.1" . PHP_EOL .
"HOST_PORT=8080" . PHP_EOL .
"TOKEN=changeme" . PHP_EOL .
"STORAGE_TYPE=filesystem" . PHP_EOL .
"JSON_PATH=json/verify.json" . PHP_EOL .
"# SQLite configuration" . PHP_EOL .
"#DB_DSN=sqlite:verify.db" . PHP_EOL .
"#DB_USERNAME=" . PHP_EOL .
"#DB_PASSWORD=" . PHP_EOL .
"#DB_OPTIONS=" . PHP_EOL .
"# MySQL configuration" . PHP_EOL .
"DB_DSN=mysql:host=127.0.0.1;port=3306;dbname=verify_list" . PHP_EOL .
"DB_PORT=3306" . PHP_EOL .
"DB_USERNAME=your_username" . PHP_EOL .
"DB_PASSWORD=your_password" . PHP_EOL .
"#DB_OPTIONS={\"option1\":\"value1\",\"option2\":\"value2\"}"
"SS14_JSON_PATH=json/ss14verify.json" . PHP_EOL .
"# SQLite configuration" . PHP_EOL .
"#DB_DSN=sqlite:verify.db" . PHP_EOL .
"#DB_USERNAME=" . PHP_EOL .
"#DB_PASSWORD=" . PHP_EOL .
"#DB_OPTIONS=" . PHP_EOL .
"# MySQL configuration" . PHP_EOL .
"#DB_DSN=mysql:host=127.0.0.1;port=3306;dbname=verify_list" . PHP_EOL .
"#DB_PORT=3306" . PHP_EOL .
"#DB_USERNAME=your_username" . PHP_EOL .
"#DB_PASSWORD=your_password" . PHP_EOL .
"#DB_OPTIONS={\"option1\":\"value1\",\"option2\":\"value2\"}" . PHP_EOL
);
}

$dotenv = Dotenv::createImmutable(__DIR__ . '\\..\\..\\');
$dotenv->load();
$loaded = Dotenv::createImmutable($directory)->load();

$env = $_ENV;
$env = [];
foreach (self::ENV_DEFAULTS as $key => $default) {
$value = $_ENV[$key] ?? $_SERVER[$key] ?? null;
if ($value === null) $value = getenv($key);
if ($value === false) $value = $loaded[$key] ?? null;
$env[$key] = $value === null ? $default : (string) $value;
}
if ($env['TOKEN'] === 'changeme' && $env['HOST_ADDR'] !== '127.0.0.1') {
die("Cannot use default token with non-localhost address! ");
throw new Exception('Cannot use default token with non-localhost address!');
}
return [
'HOST_ADDR' => $env['HOST_ADDR'],
'HOST_PORT' => $env['HOST_PORT'],
'TOKEN' => $env['TOKEN'],
'DB_DSN' => $env['DB_DSN'],
'DB_PORT' => $env['DB_PORT'],
'DB_USERNAME' => $env['DB_USERNAME'],
'DB_PASSWORD' => $env['DB_PASSWORD'],
'DB_OPTIONS' => $env['DB_OPTIONS'] ?? ''
];
return $env;
}

/**
Expand Down
67 changes: 67 additions & 0 deletions tests/isolated/EnvConfigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use VerifierServer\PersistentState;

final class EnvConfigTest extends TestCase
{
private string $directory;

protected function setUp(): void
{
$this->directory = sys_get_temp_dir() . '/verifier-env-' . bin2hex(random_bytes(4));
mkdir($this->directory);
$this->clearEnvironment();
}

protected function tearDown(): void
{
@unlink($this->directory . '/.env');
rmdir($this->directory);
$this->clearEnvironment();
}

/** Dotenv is immutable: a value already in the environment wins, so each test starts clean. */
private function clearEnvironment(): void
{
foreach (array_keys(PersistentState::ENV_DEFAULTS) as $key) {
unset($_ENV[$key], $_SERVER[$key]);
putenv($key);
}
}

public function testMissingFileIsCreatedWithLocalhostDefaultsAndNoWarnings(): void
{
$config = PersistentState::loadEnvConfig($this->directory);
self::assertFileExists($this->directory . '/.env');
self::assertSame(PersistentState::ENV_DEFAULTS, $config);
self::assertStringNotContainsString("\nDB_DSN=", file_get_contents($this->directory . '/.env'), 'no live database line in the generated file');
}

public function testFileValuesAreReadFromTheGivenDirectory(): void
{
file_put_contents($this->directory . '/.env', "HOST_ADDR=0.0.0.0\nHOST_PORT=16261\nTOKEN=secret\nSTORAGE_TYPE=database\nDB_DSN=sqlite:verify.db\n");
$config = PersistentState::loadEnvConfig($this->directory);
self::assertSame('0.0.0.0', $config['HOST_ADDR']);
self::assertSame('16261', $config['HOST_PORT']);
self::assertSame('secret', $config['TOKEN']);
self::assertSame('database', $config['STORAGE_TYPE']);
self::assertSame('sqlite:verify.db', $config['DB_DSN']);
self::assertSame('', $config['DB_USERNAME']);
self::assertSame('json/verify.json', $config['JSON_PATH']);
}

public function testRealEnvironmentOverridesTheFile(): void
{
file_put_contents($this->directory . '/.env', "TOKEN=from-file\n");
$_SERVER['TOKEN'] = 'from-environment'; // how a real environment variable appears to dotenv
self::assertSame('from-environment', PersistentState::loadEnvConfig($this->directory)['TOKEN']);
}

public function testDefaultTokenOnAPublicAddressIsRefused(): void
{
file_put_contents($this->directory . '/.env', "HOST_ADDR=0.0.0.0\nTOKEN=changeme\n");
$this->expectException(Exception::class);
PersistentState::loadEnvConfig($this->directory);
}
}
Loading