feat: self-host TweetNaCl, add server-side PoW captcha (PHP), activate categoriesService
This commit is contained in:
3
docs/pow-server/.htaccess
Normal file
3
docs/pow-server/.htaccess
Normal file
@@ -0,0 +1,3 @@
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
43
docs/pow-server/README.md
Normal file
43
docs/pow-server/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# PoW Captcha Server
|
||||
|
||||
PHP-basierter Proof-of-Work Captcha Server für dgray.io.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Subdomain `pow.dgray.io` auf den Server zeigen
|
||||
2. Dateien in das Web-Root kopieren
|
||||
3. Secret setzen:
|
||||
```bash
|
||||
# In .env oder Apache/Nginx config:
|
||||
SetEnv POW_SECRET $(openssl rand -hex 32)
|
||||
```
|
||||
Oder direkt in `config.php` den Wert von `POW_SECRET` ändern.
|
||||
|
||||
4. Testen:
|
||||
```bash
|
||||
curl https://pow.dgray.io/challenge
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /challenge
|
||||
Gibt eine signierte Challenge zurück.
|
||||
|
||||
### POST /verify
|
||||
Prüft die Lösung. Body (JSON):
|
||||
```json
|
||||
{
|
||||
"challenge": "...",
|
||||
"difficulty": 4,
|
||||
"nonce": 12345,
|
||||
"signature": "...",
|
||||
"timestamp": 1700000000000
|
||||
}
|
||||
```
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- HMAC-SHA256 signierte Challenges (nicht fälschbar)
|
||||
- TTL: 2 Minuten
|
||||
- CORS: nur `https://dgray.io`
|
||||
- `hash_equals()` gegen Timing-Attacks
|
||||
20
docs/pow-server/challenge.php
Normal file
20
docs/pow-server/challenge.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$challenge = bin2hex(random_bytes(16));
|
||||
$timestamp = (int)(microtime(true) * 1000);
|
||||
$signature = hash_hmac('sha256', "$challenge:$timestamp:" . POW_DIFFICULTY, POW_SECRET);
|
||||
|
||||
echo json_encode([
|
||||
'challenge' => $challenge,
|
||||
'difficulty' => POW_DIFFICULTY,
|
||||
'timestamp' => $timestamp,
|
||||
'signature' => $signature,
|
||||
'expires_in' => POW_TTL_SECONDS,
|
||||
]);
|
||||
4
docs/pow-server/config.php
Normal file
4
docs/pow-server/config.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
define('POW_SECRET', getenv('POW_SECRET') ?: 'CHANGE_ME_TO_A_RANDOM_64_CHAR_HEX_STRING');
|
||||
define('POW_DIFFICULTY', 4);
|
||||
define('POW_TTL_SECONDS', 120);
|
||||
25
docs/pow-server/index.php
Normal file
25
docs/pow-server/index.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: https://dgray.io');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$uri = rtrim($uri, '/');
|
||||
|
||||
switch ($uri) {
|
||||
case '/challenge':
|
||||
require __DIR__ . '/challenge.php';
|
||||
break;
|
||||
case '/verify':
|
||||
require __DIR__ . '/verify.php';
|
||||
break;
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Not found']);
|
||||
}
|
||||
47
docs/pow-server/verify.php
Normal file
47
docs/pow-server/verify.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$challenge = $input['challenge'] ?? null;
|
||||
$difficulty = $input['difficulty'] ?? POW_DIFFICULTY;
|
||||
$nonce = $input['nonce'] ?? null;
|
||||
$signature = $input['signature'] ?? null;
|
||||
$timestamp = $input['timestamp'] ?? null;
|
||||
|
||||
if (!$challenge || $nonce === null || !$signature || !$timestamp) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Missing fields']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$now = (int)(microtime(true) * 1000);
|
||||
if ($now - $timestamp > POW_TTL_SECONDS * 1000) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Challenge expired']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$expected = hash_hmac('sha256', "$challenge:$timestamp:$difficulty", POW_SECRET);
|
||||
if (!hash_equals($expected, $signature)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid signature']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$hash = hash('sha256', $challenge . $nonce);
|
||||
$prefix = str_repeat('0', $difficulty);
|
||||
|
||||
if (strpos($hash, $prefix) !== 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid proof']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
Reference in New Issue
Block a user