Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ C/Users/
dependencies.lock
.vscode/settings.json
*.map
_codeql_detected_source_root
198 changes: 198 additions & 0 deletions include/Builtin_Pages.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ String OTAServerIndex =
"<body>"
"<div style='width:400px;padding:20px;border-radius:10px;border:solid 2px #e0e0e0;margin:auto;margin-top:20px;background-color:#fff;'>"
"<div style='text-align:center;font-size:18px;font-weight:bold;margin-bottom:12px;'>SmartSpin2k OTA</div>"

"<div id='github-release-section' style='margin-bottom:20px;border-bottom:1px solid #e0e0e0;padding-bottom:20px;display:none;'>"
"<div style='text-align:center;font-size:14px;font-weight:bold;margin-bottom:10px;'>GitHub Release</div>"
"<div id='release-info' style='font-size:12px;padding:10px;background-color:#f5f5f5;border-radius:4px;'></div>"
"<button id='install-btn' onclick='downloadAndInstall()' class='btn' style='width:100%;margin-top:10px;display:none;background-color:#28a745;'>Download & Install Update</button>"
"<div id='download-progress' style='background-color:#e0e0e0;border-radius:8px;margin-top:10px;display:none;'>"
"<div id='dl-prg' style='width:0%;background-color:#007bff;padding:2px;border-radius:8px;color:white;text-align:center;'>0%</div>"
"</div>"
"</div>"

"<div style='text-align:center;font-size:14px;font-weight:bold;margin-bottom:10px;'>Manual Upload</div>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload-form'>"
"<input type='file' name='update' style='margin-bottom:10px;'>"
"<input type='submit' value='Update' class='btn' style='width:100%;'>"
Expand All @@ -80,6 +91,193 @@ String OTAServerIndex =
"<script>"
"var prg = document.getElementById('prg');"
"var form = document.getElementById('upload-form');"
"var releaseInfo = document.getElementById('release-info');"
"var installBtn = document.getElementById('install-btn');"
"var downloadProgress = document.getElementById('download-progress');"
"var dlPrg = document.getElementById('dl-prg');"
"var githubSection = document.getElementById('github-release-section');"
"var currentReleaseData = null;"

"function compareVersions(current, latest) {"
"var parseCurrent = current.match(/(\\d+)\\.(\\d+)\\.(\\d+)/);"
"var parseLatest = latest.match(/(\\d+)\\.(\\d+)\\.(\\d+)/);"
"if (!parseCurrent || !parseLatest) return false;"
"var cYear = parseInt(parseCurrent[1]), cMonth = parseInt(parseCurrent[2]), cDay = parseInt(parseCurrent[3]);"
"var lYear = parseInt(parseLatest[1]), lMonth = parseInt(parseLatest[2]), lDay = parseInt(parseLatest[3]);"
"if (lYear !== cYear) return lYear > cYear;"
"if (lMonth !== cMonth) return lMonth > cMonth;"
"return lDay > cDay;"
"}"

"function checkForUpdates() {"
"fetch('/configJSON')"
".then(r => r.json())"
".then(config => {"
"var currentVersion = config.firmwareVersion || 'unknown';"
"return fetch('https://api.github.com/repos/doudar/SmartSpin2k/releases/latest')"
".then(r => r.json())"
".then(release => {"
"var tagName = release.tag_name || '';"
"var releaseName = release.name || '';"
"var assetUrl = '';"
"if (release.assets) {"
"for (var i = 0; i < release.assets.length; i++) {"
"var assetName = release.assets[i].name || '';"
"if (assetName.endsWith('.bin.zip') || assetName.endsWith('.bin')) {"
"assetUrl = release.assets[i].browser_download_url;"
"break;"
"}"
"}"
"}"
"if (!assetUrl) {"
"console.log('No firmware asset found in release');"
"return;"
"}"
"var isNewer = compareVersions(currentVersion, tagName);"
"currentReleaseData = {"
"currentVersion: currentVersion,"
"latestVersion: tagName,"
"releaseName: releaseName,"
"assetUrl: assetUrl,"
"isZipped: assetUrl.endsWith('.zip')"
"};"
"githubSection.style.display = 'block';"
"let msg = '<strong>Current:</strong> ' + currentVersion + '<br>';"
"msg += '<strong>Latest:</strong> ' + tagName + '<br>';"
"if (isNewer) {"
"msg += '<strong style=\"color:#28a745;\">New version available!</strong><br>';"
"msg += '<strong>Release:</strong> ' + releaseName;"
"installBtn.style.display = 'block';"
"} else {"
"msg += '<strong style=\"color:#007bff;\">You are up to date!</strong>';"
"}"
"releaseInfo.innerHTML = msg;"
"});"
"})"
".catch(e => {"
"console.error('Error checking for updates:', e);"
"});"
"}"

"window.addEventListener('load', function() {"
"checkForUpdates();"
"});"

"function extractBinFromZip(zipBlob, callback) {"
"var reader = new FileReader();"
"reader.onload = function(e) {"
"var data = new Uint8Array(e.target.result);"
"var pos = 0;"
"while (pos < data.length - 30) {"
"if (data[pos] === 0x50 && data[pos+1] === 0x4B && data[pos+2] === 0x03 && data[pos+3] === 0x04) {"
"var nameLen = data[pos+26] | (data[pos+27] << 8);"
"var extraLen = data[pos+28] | (data[pos+29] << 8);"
"var compSize = data[pos+18] | (data[pos+19] << 8) | (data[pos+20] << 16) | (data[pos+21] << 24);"
"var fileName = String.fromCharCode.apply(null, data.slice(pos+30, pos+30+nameLen));"
"if (fileName.endsWith('.bin')) {"
"var fileStart = pos + 30 + nameLen + extraLen;"
"var binData = data.slice(fileStart, fileStart + compSize);"
"callback(new Blob([binData], {type: 'application/octet-stream'}));"
"return;"
"}"
"pos += 30 + nameLen + extraLen + compSize;"
"} else {"
"pos++;"
"}"
"}"
"callback(null);"
"};"
"reader.readAsArrayBuffer(zipBlob);"
"}"

"function uploadFirmware(blob) {"
"releaseInfo.innerHTML = 'Uploading to device...';"
"dlPrg.style.backgroundColor = '#28a745';"
"dlPrg.innerHTML = 'Uploading...';"
"var formData = new FormData();"
"formData.append('update', blob, 'firmware.bin');"
"var uploadXhr = new XMLHttpRequest();"
"uploadXhr.open('POST', '/update');"
"uploadXhr.upload.onprogress = function(e) {"
"if (e.lengthComputable) {"
"var percentComplete = Math.round((e.loaded / e.total) * 100);"
"prg.style.width = percentComplete + '%';"
"prg.innerHTML = percentComplete + '%';"
"}"
"};"
"uploadXhr.onload = function() {"
"if (uploadXhr.status === 200) {"
"prg.style.backgroundColor = '#04AA6D';"
"prg.innerHTML = 'Success! Device is rebooting...';"
"releaseInfo.innerHTML = '<strong style=\"color:#28a745;\">Update successful! Device is rebooting...</strong>';"
"downloadProgress.style.display = 'none';"
"} else {"
"prg.innerHTML = 'Upload failed';"
"releaseInfo.innerHTML = '<strong style=\"color:#dc3545;\">Upload failed. Please try again.</strong>';"
"installBtn.style.display = 'block';"
"}"
"};"
"uploadXhr.onerror = function() {"
"releaseInfo.innerHTML = '<strong style=\"color:#dc3545;\">Upload error. Please try again.</strong>';"
"installBtn.style.display = 'block';"
"};"
"uploadXhr.send(formData);"
"}"

"function downloadAndInstall() {"
"if (!currentReleaseData || !currentReleaseData.assetUrl) {"
"releaseInfo.innerHTML = '<strong style=\"color:#dc3545;\">No release data available</strong>';"
"return;"
"}"
"if (confirm('This will download and install firmware version ' + currentReleaseData.latestVersion + '. The device will reboot. Continue?')) {"
"installBtn.style.display = 'none';"
"downloadProgress.style.display = 'block';"
"releaseInfo.innerHTML = 'Downloading firmware from GitHub...';"
"dlPrg.style.width = '0%';"
"dlPrg.innerHTML = '0%';"
"var xhr = new XMLHttpRequest();"
"xhr.open('GET', '/downloadFirmware?url=' + encodeURIComponent(currentReleaseData.assetUrl), true);"
"xhr.responseType = 'blob';"
"xhr.onprogress = function(e) {"
"if (e.lengthComputable) {"
"var percentComplete = Math.round((e.loaded / e.total) * 100);"
"dlPrg.style.width = percentComplete + '%';"
"dlPrg.innerHTML = percentComplete + '%';"
"}"
"};"
"xhr.onload = function() {"
"if (xhr.status === 200) {"
"var blob = xhr.response;"
"if (currentReleaseData.isZipped) {"
"releaseInfo.innerHTML = 'Extracting firmware from zip...';"
"dlPrg.innerHTML = 'Extracting...';"
"extractBinFromZip(blob, function(binBlob) {"
"if (binBlob) {"
"uploadFirmware(binBlob);"
"} else {"
"releaseInfo.innerHTML = '<strong style=\"color:#dc3545;\">Failed to extract firmware from zip.</strong>';"
"installBtn.style.display = 'block';"
"downloadProgress.style.display = 'none';"
"}"
"});"
"} else {"
"uploadFirmware(blob);"
"}"
"} else {"
"releaseInfo.innerHTML = '<strong style=\"color:#dc3545;\">Download failed. Please try again.</strong>';"
"installBtn.style.display = 'block';"
"downloadProgress.style.display = 'none';"
"}"
"};"
"xhr.onerror = function() {"
"releaseInfo.innerHTML = '<strong style=\"color:#dc3545;\">Download error. Please check your internet connection.</strong>';"
"installBtn.style.display = 'block';"
"downloadProgress.style.display = 'none';"
"};"
"xhr.send();"
"}"
"}"

"form.addEventListener('submit', e=>{"
"e.preventDefault();"
"var data = new FormData(form);"
Expand Down
58 changes: 58 additions & 0 deletions src/HTTP_Server_Basic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,62 @@ void HTTP_Server::start() {
ss2k->rebootFlag = true;
});

server.on("/downloadFirmware", HTTP_ANY, []() {
if (!server.hasArg("url")) {
server.send(400, "text/plain", "Missing url parameter");
return;
}
String firmwareUrl = server.arg("url");
SS2K_LOG(HTTP_SERVER_LOG_TAG, "Proxying firmware download from: %s", firmwareUrl.c_str());

HTTPClient http;
WiFiClientSecure client;

// Try with bundled certificate first
client.setCACert(rootCACertificate);
http.begin(client, firmwareUrl);
http.addHeader("User-Agent", "SmartSpin2k");

int httpCode = http.GET();

// If certificate validation fails, retry without validation
// This ensures updates work even if bundled cert expires
if (httpCode == HTTPC_ERROR_CONNECTION_REFUSED || httpCode < 0) {
SS2K_LOG(HTTP_SERVER_LOG_TAG, "Certificate validation failed, retrying without validation");
http.end();
client.setInsecure(); // Skip certificate validation
http.begin(client, firmwareUrl);
http.addHeader("User-Agent", "SmartSpin2k");
httpCode = http.GET();
}

if (httpCode == HTTP_CODE_OK) {
int len = http.getSize();
WiFiClient * stream = http.getStreamPtr();

server.setContentLength(len);
server.send(200, "application/octet-stream", "");

uint8_t buff[512];
while (http.connected() && (len > 0 || len == -1)) {
size_t size = stream->available();
if (size) {
int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size));
server.client().write(buff, c);
if (len > 0) {
len -= c;
}
}
delay(1);
}
SS2K_LOG(HTTP_SERVER_LOG_TAG, "Firmware proxy download complete");
} else {
SS2K_LOG(HTTP_SERVER_LOG_TAG, "Firmware proxy download failed: %d", httpCode);
server.send(httpCode, "text/plain", "Failed to download firmware");
}
http.end();
});

server.on("/hrslider", []() {
String value = server.arg("value");
if (value == "enable") {
Expand Down Expand Up @@ -733,6 +789,8 @@ void HTTP_Server::stop() {
server.close();
}



// github fingerprint
// 70:94:DE:DD:E6:C4:69:48:3A:92:70:A1:48:56:78:2D:18:64:E0:B7

Expand Down