Dateien nach "/" hochladen
This commit is contained in:
+297
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
/**
|
||||
* ============================================================
|
||||
* db-bridge.php - HTTP-Brücke für JSON Studio & MariaDB Workbench
|
||||
* ============================================================
|
||||
*
|
||||
* Ein Browser kann sich nicht direkt mit MariaDB/MySQL verbinden.
|
||||
* Dieses Skript nimmt die Anfragen der Weboberfläche entgegen und
|
||||
* reicht sie an die Datenbank weiter.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* VORAUSSETZUNG
|
||||
* ------------------------------------------------------------
|
||||
* PHP 8.0 oder neuer (verwendet str_contains und Arrow-Funktionen)
|
||||
* sowie die PDO-MySQL-Erweiterung.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* EINRICHTUNG
|
||||
* ------------------------------------------------------------
|
||||
* 1. Unten im Abschnitt KONFIGURATION die Zugangsdaten eintragen.
|
||||
* 2. Einen eigenen API-Schlüssel setzen (beliebige lange Zeichenfolge).
|
||||
* 3. ALLOWED_ORIGIN auf die Adresse setzen, unter der die
|
||||
* HTML-Datei ausgeliefert wird.
|
||||
* 4. Datei auf den Server legen, Adresse im Profil der
|
||||
* Weboberfläche eintragen.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* SICHERHEIT - bitte lesen
|
||||
* ------------------------------------------------------------
|
||||
* - Lege einen eigenen Datenbankbenutzer an, der nur auf die
|
||||
* benötigte Datenbank zugreifen darf. Niemals 'root' verwenden.
|
||||
* - Betreibe das Skript ausschließlich über HTTPS, sonst gehen
|
||||
* API-Schlüssel und Daten im Klartext über die Leitung.
|
||||
* - Der API-Schlüssel liegt im Browser-Speicher des Nutzers.
|
||||
* Behandle ihn wie ein Passwort und tausche ihn aus, wenn ein
|
||||
* Rechner abhandenkommt.
|
||||
* - Schreibende Aktionen sind über WRITES_ALLOWED abschaltbar.
|
||||
* Wenn du nur lesen willst, setze es auf false.
|
||||
* - 'query' erlaubt bewusst nur SELECT-Abfragen.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* PROTOKOLL
|
||||
* ------------------------------------------------------------
|
||||
* POST application/json, Header X-Api-Key
|
||||
*
|
||||
* {"action":"tables"}
|
||||
* -> {"tables":["kunden","artikel"]}
|
||||
*
|
||||
* {"action":"columns","table":"kunden"}
|
||||
* -> {"columns":[{"name":"id","type":"int(11)"}]}
|
||||
*
|
||||
* {"action":"select","table":"kunden","limit":500}
|
||||
* -> {"rows":[{...}]}
|
||||
*
|
||||
* {"action":"query","sql":"SELECT ...","limit":500}
|
||||
* -> {"rows":[{...}]}
|
||||
*
|
||||
* {"action":"insert","table":"kunden","rows":[{...}],
|
||||
* "mode":"append|replace|upsert","createIfMissing":true,
|
||||
* "createSql":"CREATE TABLE ..."}
|
||||
* -> {"inserted":42}
|
||||
*
|
||||
* Fehler: {"error":"Beschreibung"}
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/* ===================== KONFIGURATION ===================== */
|
||||
|
||||
const DB_HOST = '127.0.0.1';
|
||||
const DB_PORT = 3306;
|
||||
const DB_NAME = 'meine_datenbank'; // Vorgabe, per "database" im Request überschreibbar
|
||||
const DB_USER = 'workbench';
|
||||
const DB_PASS = 'HIER_PASSWORT_EINTRAGEN';
|
||||
|
||||
const API_KEY = 'HIER_EINEN_LANGEN_ZUFAELLIGEN_SCHLUESSEL_EINTRAGEN';
|
||||
|
||||
// Adresse, unter der die HTML-Datei läuft. '*' nur zum Ausprobieren verwenden.
|
||||
const ALLOWED_ORIGIN = 'https://tools.example.de';
|
||||
|
||||
// Schreibende Aktionen (insert) erlauben?
|
||||
const WRITES_ALLOWED = true;
|
||||
|
||||
// Nur diese Datenbanken dürfen angesprochen werden. Leer = nur DB_NAME.
|
||||
const ALLOWED_DATABASES = [];
|
||||
|
||||
// Obergrenzen
|
||||
const MAX_LIMIT = 10000;
|
||||
const MAX_ROWS_PER_INSERT = 20000;
|
||||
|
||||
/* =================== AB HIER NICHTS MEHR ================= */
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: ' . ALLOWED_ORIGIN);
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
||||
|
||||
function fail(string $message, int $code = 400): void {
|
||||
http_response_code($code);
|
||||
echo json_encode(['error' => $message], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
function ok(array $payload): void {
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
fail('Nur POST wird unterstützt.', 405);
|
||||
}
|
||||
|
||||
// Schlüsselprüfung in konstanter Zeit
|
||||
$sentKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||
if (!hash_equals(API_KEY, $sentKey)) {
|
||||
fail('Ungültiger oder fehlender API-Schlüssel.', 401);
|
||||
}
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$req = json_decode($raw, true);
|
||||
if (!is_array($req)) {
|
||||
fail('Anfrage ist kein gültiges JSON.');
|
||||
}
|
||||
|
||||
$action = $req['action'] ?? '';
|
||||
|
||||
// Zieldatenbank bestimmen
|
||||
$database = trim((string)($req['database'] ?? '')) ?: DB_NAME;
|
||||
$allowed = ALLOWED_DATABASES ?: [DB_NAME];
|
||||
if (!in_array($database, $allowed, true)) {
|
||||
fail('Diese Datenbank ist nicht freigegeben: ' . $database, 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', DB_HOST, DB_PORT, $database),
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
fail('Verbindung fehlgeschlagen: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
/** Tabellen-/Spaltennamen dürfen nicht als Parameter gebunden werden - daher streng prüfen. */
|
||||
function safeIdentifier(string $name): string {
|
||||
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_$]*$/', $name)) {
|
||||
fail('Unzulässiger Bezeichner: ' . $name);
|
||||
}
|
||||
return '`' . $name . '`';
|
||||
}
|
||||
|
||||
function clampLimit($value): int {
|
||||
$limit = (int)($value ?? 500);
|
||||
if ($limit < 1) $limit = 1;
|
||||
if ($limit > MAX_LIMIT) $limit = MAX_LIMIT;
|
||||
return $limit;
|
||||
}
|
||||
|
||||
/** Werte für die Datenbank aufbereiten: Arrays/Objekte werden zu JSON-Text. */
|
||||
function normalizeValue($value) {
|
||||
if (is_array($value) || is_object($value)) {
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
if (is_bool($value)) return $value ? 1 : 0;
|
||||
return $value;
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
|
||||
case 'tables': {
|
||||
$rows = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_NUM);
|
||||
ok(['tables' => array_map(fn($r) => $r[0], $rows)]);
|
||||
}
|
||||
|
||||
case 'columns': {
|
||||
$table = safeIdentifier((string)($req['table'] ?? ''));
|
||||
$rows = $pdo->query("SHOW COLUMNS FROM $table")->fetchAll();
|
||||
ok(['columns' => array_map(
|
||||
fn($r) => ['name' => $r['Field'], 'type' => $r['Type'], 'null' => $r['Null'], 'key' => $r['Key']],
|
||||
$rows
|
||||
)]);
|
||||
}
|
||||
|
||||
case 'select': {
|
||||
$table = safeIdentifier((string)($req['table'] ?? ''));
|
||||
$limit = clampLimit($req['limit'] ?? null);
|
||||
$stmt = $pdo->query("SELECT * FROM $table LIMIT $limit");
|
||||
ok(['rows' => $stmt->fetchAll()]);
|
||||
}
|
||||
|
||||
case 'query': {
|
||||
$sql = trim((string)($req['sql'] ?? ''));
|
||||
if ($sql === '') fail('Keine Abfrage übergeben.');
|
||||
|
||||
// Bewusst nur lesende Abfragen zulassen
|
||||
if (!preg_match('/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)\b/i', $sql)) {
|
||||
fail('Über "query" sind nur lesende Abfragen erlaubt (SELECT, SHOW, DESCRIBE, EXPLAIN).');
|
||||
}
|
||||
if (str_contains($sql, ';') && !preg_match('/;\s*$/', $sql)) {
|
||||
fail('Mehrere Anweisungen in einer Abfrage sind nicht erlaubt.');
|
||||
}
|
||||
$sql = rtrim(rtrim($sql), ';');
|
||||
|
||||
$limit = clampLimit($req['limit'] ?? null);
|
||||
if (!preg_match('/\bLIMIT\s+\d+/i', $sql)) {
|
||||
$sql .= " LIMIT $limit";
|
||||
}
|
||||
ok(['rows' => $pdo->query($sql)->fetchAll()]);
|
||||
}
|
||||
|
||||
case 'insert': {
|
||||
if (!WRITES_ALLOWED) fail('Schreibende Zugriffe sind auf diesem Server abgeschaltet.', 403);
|
||||
|
||||
$tableName = (string)($req['table'] ?? '');
|
||||
$table = safeIdentifier($tableName);
|
||||
$rows = $req['rows'] ?? [];
|
||||
$mode = $req['mode'] ?? 'append';
|
||||
|
||||
if (!is_array($rows) || count($rows) === 0) fail('Keine Zeilen übergeben.');
|
||||
if (count($rows) > MAX_ROWS_PER_INSERT) {
|
||||
fail('Zu viele Zeilen auf einmal (Grenze: ' . MAX_ROWS_PER_INSERT . ').');
|
||||
}
|
||||
if (!in_array($mode, ['append', 'replace', 'upsert'], true)) fail('Unbekannter Modus: ' . $mode);
|
||||
|
||||
// Tabelle bei Bedarf anlegen
|
||||
$exists = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ?');
|
||||
$exists->execute([$database, $tableName]);
|
||||
if ((int)$exists->fetchColumn() === 0) {
|
||||
if (empty($req['createIfMissing'])) {
|
||||
fail('Tabelle existiert nicht: ' . $tableName);
|
||||
}
|
||||
$createSql = (string)($req['createSql'] ?? '');
|
||||
if (!preg_match('/^\s*CREATE\s+TABLE\b/i', $createSql)) {
|
||||
fail('Kein gültiges CREATE TABLE übermittelt.');
|
||||
}
|
||||
$pdo->exec($createSql);
|
||||
}
|
||||
|
||||
// Spalten aus der ersten Zeile ableiten und gegen die echte Tabelle prüfen
|
||||
$firstRow = $rows[0];
|
||||
if (!is_array($firstRow)) fail('Zeilen müssen Objekte mit Spaltennamen sein.');
|
||||
|
||||
$tableCols = array_column($pdo->query("SHOW COLUMNS FROM $table")->fetchAll(), 'Field');
|
||||
$columns = array_values(array_filter(array_keys($firstRow), fn($c) => in_array($c, $tableCols, true)));
|
||||
if (count($columns) === 0) fail('Keine der übergebenen Spalten existiert in der Tabelle.');
|
||||
|
||||
$quoted = array_map('safeIdentifier', $columns);
|
||||
$placeholder = '(' . implode(', ', array_fill(0, count($columns), '?')) . ')';
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
if ($mode === 'replace') {
|
||||
$pdo->exec("TRUNCATE TABLE $table");
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO $table (" . implode(', ', $quoted) . ") VALUES $placeholder";
|
||||
if ($mode === 'upsert') {
|
||||
$updates = array_map(fn($c) => "$c = VALUES($c)", $quoted);
|
||||
$sql .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
|
||||
}
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
$inserted = 0;
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) continue;
|
||||
$values = [];
|
||||
foreach ($columns as $col) {
|
||||
$values[] = normalizeValue($row[$col] ?? null);
|
||||
}
|
||||
$stmt->execute($values);
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
ok(['inserted' => $inserted, 'columns' => $columns, 'mode' => $mode]);
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
fail('Unbekannte Aktion: ' . $action);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
fail('Datenbankfehler: ' . $e->getMessage(), 500);
|
||||
} catch (Throwable $e) {
|
||||
fail('Fehler: ' . $e->getMessage(), 500);
|
||||
}
|
||||
Reference in New Issue
Block a user