|
<?php |
|
/** |
|
* SwitchBot Webhook Deleter (Single File) |
|
* Features: |
|
* - Version Selector (v1.0 vs v1.1) |
|
* - Automatic Signature Generation for v1.1 |
|
* - CORS Bypass via PHP Proxy |
|
* - Full Logging |
|
*/ |
|
|
|
// --- PHP BACKEND (API PROXY) --- |
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['action']) && $_GET['action'] === 'execute') { |
|
// Clean buffer to ensure valid JSON response |
|
ob_clean(); |
|
header('Content-Type: application/json'); |
|
|
|
// 1. Get Input |
|
$input = json_decode(file_get_contents('php://input'), true); |
|
$version = $input['version'] ?? 'v1.1'; |
|
$token = trim($input['token'] ?? ''); |
|
$secret = trim($input['secret'] ?? ''); |
|
$urlToDelete = trim($input['url'] ?? ''); |
|
|
|
if (empty($token) || empty($urlToDelete)) { |
|
echo json_encode(['error' => 'Missing Token or URL']); |
|
exit; |
|
} |
|
|
|
// 2. Prepare Payload (Common for both versions) |
|
// Docs: https://github.com/OpenWonderLabs/SwitchBotAPI#delete-webhook |
|
$payloadData = [ |
|
'action' => 'deleteWebhook', |
|
'url' => $urlToDelete |
|
]; |
|
$payload = json_encode($payloadData); |
|
|
|
// 3. Configure Headers & Endpoint based on Version |
|
$headers = [ |
|
"Content-Type: application/json; charset=utf8", |
|
"Content-Length: " . strlen($payload) |
|
]; |
|
|
|
if ($version === 'v1.0') { |
|
// --- v1.0 Logic --- |
|
// Endpoint: https://api.switch-bot.com/v1.0/webhook/deleteWebhook |
|
// Auth: Authorization header only |
|
$apiUrl = "https://api.switch-bot.com/v1.0/webhook/deleteWebhook"; |
|
$headers[] = "Authorization: " . $token; |
|
|
|
} else { |
|
// --- v1.1 Logic --- |
|
// Endpoint: https://api.switch-bot.com/v1.1/webhook/deleteWebhook |
|
// Auth: Authorization + Sign + Nonce + t |
|
if (empty($secret)) { |
|
echo json_encode(['error' => 'Secret Key is required for v1.1']); |
|
exit; |
|
} |
|
|
|
$apiUrl = "https://api.switch-bot.com/v1.1/webhook/deleteWebhook"; |
|
|
|
// Generate Signature |
|
$nonce = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', |
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), |
|
mt_rand(0, 0xffff), |
|
mt_rand(0, 0x0fff) | 0x4000, |
|
mt_rand(0, 0x3fff) | 0x8000, |
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) |
|
); |
|
$t = time() * 1000; |
|
$stringToSign = $token . $t . $nonce; |
|
$sign = strtoupper(base64_encode(hash_hmac('sha256', utf8_encode($stringToSign), utf8_encode($secret), true))); |
|
|
|
$headers[] = "Authorization: " . $token; |
|
$headers[] = "sign: " . $sign; |
|
$headers[] = "nonce: " . $nonce; |
|
$headers[] = "t: " . $t; |
|
} |
|
|
|
// 4. Execute cURL |
|
$ch = curl_init($apiUrl); |
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); |
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); |
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); |
|
// Optional: SSL Verify. Set to false ONLY if your server has certificate issues. |
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); |
|
|
|
$response = curl_exec($ch); |
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); |
|
$curlError = curl_error($ch); |
|
|
|
// Debug info: Mask sensitive data for logs |
|
$debugHeaders = $headers; |
|
// We don't want to log the full token/secret in plain text if we can avoid it, |
|
// but useful for debugging connectivity. |
|
|
|
curl_close($ch); |
|
|
|
// 5. Return Response |
|
if ($curlError) { |
|
echo json_encode(['error' => 'cURL Error: ' . $curlError]); |
|
} else { |
|
echo json_encode([ |
|
'version_used' => $version, |
|
'endpoint_used' => $apiUrl, |
|
'http_code' => $httpCode, |
|
'response' => json_decode($response), |
|
'payload_sent' => $payloadData |
|
]); |
|
} |
|
exit; |
|
} |
|
?> |
|
|
|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>SwitchBot Webhook Tool</title> |
|
<style> |
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; background-color: #121212; color: #e0e0e0; } |
|
.card { background: #1e1e1e; padding: 2rem; border: 1px solid #333; border-radius: 8px; } |
|
h2 { margin-top: 0; color: #ff6b6b; border-bottom: 1px solid #333; padding-bottom: 1rem; } |
|
|
|
.form-group { margin-bottom: 1.5rem; } |
|
label { display: block; margin-bottom: 0.5rem; color: #aaa; font-size: 0.9rem; } |
|
input[type="text"], select { width: 100%; padding: 12px; background: #2d2d2d; border: 1px solid #444; color: #fff; box-sizing: border-box; font-family: monospace; font-size: 1rem; border-radius: 4px; } |
|
input:focus, select:focus { border-color: #ff6b6b; outline: none; } |
|
|
|
button { background-color: #ff6b6b; color: #000; border: none; padding: 14px 24px; cursor: pointer; font-weight: bold; width: 100%; margin-top: 1rem; border-radius: 4px; transition: background 0.2s; } |
|
button:hover { background-color: #ff5252; } |
|
button:disabled { background-color: #555; cursor: not-allowed; color: #888; } |
|
|
|
#logs { margin-top: 2rem; padding: 1rem; background: #000; border: 1px solid #333; height: 400px; overflow-y: auto; white-space: pre-wrap; color: #00ff00; font-family: monospace; font-size: 0.85rem; border-radius: 4px; } |
|
.log-entry { margin-bottom: 1em; border-bottom: 1px solid #222; padding-bottom: 0.5em; } |
|
.log-err { color: #ff5252; } |
|
.log-warn { color: #ffd700; } |
|
</style> |
|
</head> |
|
<body> |
|
|
|
<div class="card"> |
|
<h2>DELETE WEBHOOK</h2> |
|
|
|
<div class="form-group"> |
|
<label for="version">API Version (Try v1.1 first)</label> |
|
<select id="version" onchange="toggleSecretField()"> |
|
<option value="v1.1">v1.1 (Recommended - Requires Secret)</option> |
|
<option value="v1.0">v1.0 (Legacy - Token Only)</option> |
|
</select> |
|
</div> |
|
|
|
<div class="form-group"> |
|
<label>Token (Open Token)</label> |
|
<input type="text" id="token" placeholder="Your Open Token"> |
|
</div> |
|
|
|
<div class="form-group" id="secret-group"> |
|
<label>Secret Key</label> |
|
<input type="text" id="secret" placeholder="Your Secret Key"> |
|
</div> |
|
|
|
<div class="form-group"> |
|
<label>Webhook URL to Delete</label> |
|
<input type="text" id="url" placeholder="https://your-domain.com/hook"> |
|
</div> |
|
|
|
<button id="deleteBtn" onclick="executeDelete()">EXECUTE DELETE</button> |
|
|
|
<div id="logs">System Ready. Select version and enter credentials.</div> |
|
</div> |
|
|
|
<script> |
|
// UI Helper: Hide secret field if v1.0 is selected |
|
function toggleSecretField() { |
|
const ver = document.getElementById('version').value; |
|
const secretGroup = document.getElementById('secret-group'); |
|
if (ver === 'v1.0') { |
|
secretGroup.style.opacity = '0.5'; |
|
document.getElementById('secret').disabled = true; |
|
} else { |
|
secretGroup.style.opacity = '1'; |
|
document.getElementById('secret').disabled = false; |
|
} |
|
} |
|
|
|
function log(msg, type = 'info') { |
|
const logs = document.getElementById('logs'); |
|
const ts = new Date().toLocaleTimeString(); |
|
let colorClass = type === 'error' ? 'log-err' : ''; |
|
|
|
const entry = `<div class="log-entry ${colorClass}">[${ts}] ${msg}</div>`; |
|
logs.innerHTML = entry + logs.innerHTML; |
|
} |
|
|
|
async function executeDelete() { |
|
const version = document.getElementById('version').value; |
|
const token = document.getElementById('token').value.trim(); |
|
const secret = document.getElementById('secret').value.trim(); |
|
const url = document.getElementById('url').value.trim(); |
|
const btn = document.getElementById('deleteBtn'); |
|
|
|
if (!token || !url) return log("Error: Token and URL are required.", 'error'); |
|
if (version === 'v1.1' && !secret) return log("Error: v1.1 requires Secret Key.", 'error'); |
|
|
|
btn.disabled = true; |
|
btn.textContent = "Communicating..."; |
|
|
|
try { |
|
log(`Attempting DELETE via API ${version}...`); |
|
|
|
const response = await fetch('?action=execute', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ version, token, secret, url }) |
|
}); |
|
|
|
const data = await response.json(); |
|
|
|
if (data.error) { |
|
log(`Server Error: ${data.error}`, 'error'); |
|
} else { |
|
log(`HTTP Code: ${data.http_code}`); |
|
log(`Endpoint Used: ${data.endpoint_used}`); |
|
log(`Response Body:\n${JSON.stringify(data.response, null, 2)}`); |
|
|
|
// Check for success code 100 |
|
if (data.response && data.response.statusCode === 100) { |
|
log("SUCCESS: Webhook deleted successfully!"); |
|
} else if (data.response && data.response.statusCode === 190) { |
|
log("FAILURE: Device internal error or Signing error. Check Secret/Token.", 'error'); |
|
} else { |
|
log("FAILURE: See response body for details.", 'error'); |
|
} |
|
} |
|
} catch (e) { |
|
log(`JavaScript Error: ${e.message}`, 'error'); |
|
} finally { |
|
btn.disabled = false; |
|
btn.textContent = "EXECUTE DELETE"; |
|
} |
|
} |
|
</script> |
|
</body> |
|
</html> |