diff --git a/docs/api.js b/docs/api.js index 5f887e94..eff63583 100644 --- a/docs/api.js +++ b/docs/api.js @@ -9,7 +9,7 @@ function generateExamples(endpoint, method, body = null) { } return { - cURL: `curl -u user:pass -X ${method.trim()} -k https://localhost:47990${endpoint.trim()}${curlBodyString}`, + cURL: `curl -u user:pass -H "Content-Type: application/json" -X ${method.trim()} -k https://localhost:47990${endpoint.trim()}${curlBodyString}`, Python: `import json import requests from requests.auth import HTTPBasicAuth @@ -30,6 +30,7 @@ requests.${method.trim().toLowerCase()}( .then(data => console.log(data));`, PowerShell: `Invoke-RestMethod \` -SkipCertificateCheck \` + -ContentType 'application/json' \` -Uri 'https://localhost:47990${endpoint.trim()}' \` -Method ${method.trim()} \` -Headers @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('user:pass'))} diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 4ba09b88..43d6df24 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -213,6 +213,39 @@ namespace confighttp { response->write(code, tree.dump(), headers); } + /** + * @brief Validate the request content type and send bad request when mismatch. + * @param response The HTTP response object. + * @param request The HTTP request object. + * @param contentType The expected content type + */ + bool check_content_type(resp_https_t response, req_https_t request, const std::string_view &contentType) { + auto requestContentType = request->header.find("content-type"); + if (requestContentType == request->header.end()) { + bad_request(response, request, "Content type not provided"); + return false; + } + // Extract the media type part before any parameters (e.g., charset) + std::string actualContentType = requestContentType->second; + size_t semicolonPos = actualContentType.find(';'); + if (semicolonPos != std::string::npos) { + actualContentType = actualContentType.substr(0, semicolonPos); + } + + // Trim whitespace and convert to lowercase for case-insensitive comparison + boost::algorithm::trim(actualContentType); + boost::algorithm::to_lower(actualContentType); + + std::string expectedContentType(contentType); + boost::algorithm::to_lower(expectedContentType); + + if (actualContentType != expectedContentType) { + bad_request(response, request, "Content type mismatch"); + return false; + } + return true; + } + /** * @brief Get the index page. * @param response The HTTP response object. @@ -535,6 +568,9 @@ namespace confighttp { * @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}} */ void saveApp(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } @@ -602,6 +638,9 @@ namespace confighttp { * @api_examples{/api/apps/close| POST| null} */ void closeApp(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } @@ -623,6 +662,9 @@ namespace confighttp { * @api_examples{/api/apps/9999| DELETE| null} */ void deleteApp(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } @@ -703,6 +745,9 @@ namespace confighttp { * @api_examples{/api/unpair| POST| {"uuid":"1234"}} */ void unpair(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } @@ -733,6 +778,9 @@ namespace confighttp { * @api_examples{/api/clients/unpair-all| POST| null} */ void unpairAll(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } @@ -809,6 +857,9 @@ namespace confighttp { * @api_examples{/api/config| POST| {"key":"value"}} */ void saveConfig(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } @@ -855,6 +906,9 @@ namespace confighttp { * @api_examples{/api/covers/upload| POST| {"key":"igdb_1234","url":"https://images.igdb.com/igdb/image/upload/t_cover_big_2x/abc123.png"}} */ void uploadCover(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } @@ -938,6 +992,9 @@ namespace confighttp { * @api_examples{/api/password| POST| {"currentUsername":"admin","currentPassword":"admin","newUsername":"admin","newPassword":"admin","confirmNewPassword":"admin"}} */ void savePassword(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!config::sunshine.username.empty() && !authenticate(response, request)) { return; } @@ -1008,6 +1065,9 @@ namespace confighttp { * @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}} */ void savePin(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } @@ -1044,6 +1104,9 @@ namespace confighttp { * @api_examples{/api/reset-display-device-persistence| POST| null} */ void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } @@ -1063,6 +1126,9 @@ namespace confighttp { * @api_examples{/api/restart| POST| null} */ void restart(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } if (!authenticate(response, request)) { return; } diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index 66e264e3..8b0cb517 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -440,7 +440,12 @@ "Are you sure to delete " + this.apps[id].name + "?" ); if (resp) { - fetch("./api/apps/" + id, { method: "DELETE" }).then((r) => { + fetch("./api/apps/" + id, { + method: "DELETE", + headers: { + "Content-Type": "application/json" + }, + }).then((r) => { if (r.status === 200) document.location.reload(); }); } @@ -540,6 +545,9 @@ this.coverFinderBusy = true; fetch("./api/covers/upload", { method: "POST", + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify({ key: cover.key, url: cover.saveUrl, @@ -555,6 +563,9 @@ this.editForm["image-path"] = this.editForm["image-path"].toString().replace(/"/g, ''); fetch("./api/apps", { method: "POST", + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify(this.editForm), }).then((r) => { if (r.status === 200) document.location.reload(); diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index e6a85b87..f021fd71 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -371,6 +371,9 @@ return fetch("./api/config", { method: "POST", + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify(config), }).then((r) => { if (r.status === 200) { @@ -393,7 +396,10 @@ this.saved = this.restarted = false; }, 5000); fetch("./api/restart", { - method: "POST" + method: "POST", + headers: { + "Content-Type": "application/json" + } }); } }); diff --git a/src_assets/common/assets/web/password.html b/src_assets/common/assets/web/password.html index 854ee596..9f1e7194 100644 --- a/src_assets/common/assets/web/password.html +++ b/src_assets/common/assets/web/password.html @@ -92,6 +92,9 @@ this.error = null; fetch("./api/password", { method: "POST", + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify(this.passwordData), }).then((r) => { if (r.status === 200) { diff --git a/src_assets/common/assets/web/pin.html b/src_assets/common/assets/web/pin.html index a5dcdb5d..d16a5de1 100644 --- a/src_assets/common/assets/web/pin.html +++ b/src_assets/common/assets/web/pin.html @@ -39,7 +39,13 @@ let name = document.querySelector("#name-input").value; document.querySelector("#status").innerHTML = ""; let b = JSON.stringify({pin: pin, name: name}); - fetch("./api/pin", {method: "POST", body: b}) + fetch("./api/pin", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: b + }) .then((response) => response.json()) .then((response) => { if (response.status === true) { diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index 46d7218b..d742867f 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -207,7 +207,11 @@ }, closeApp() { this.closeAppPressed = true; - fetch("./api/apps/close", { method: "POST" }) + fetch("./api/apps/close", { + method: "POST", + headers: { + "Content-Type": "application/json" + } }) .then((r) => r.json()) .then((r) => { this.closeAppPressed = false; @@ -219,7 +223,12 @@ }, unpairAll() { this.unpairAllPressed = true; - fetch("./api/clients/unpair-all", { method: "POST" }) + fetch("./api/clients/unpair-all", { + method: "POST", + headers: { + "Content-Type": "application/json" + } + }) .then((r) => r.json()) .then((r) => { this.unpairAllPressed = false; @@ -231,7 +240,13 @@ }); }, unpairSingle(uuid) { - fetch("./api/clients/unpair", { method: "POST", body: JSON.stringify({ uuid }) }).then(() => { + fetch("./api/clients/unpair", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ uuid }) + }).then(() => { this.showApplyMessage = true; this.refreshClients(); }); @@ -263,11 +278,19 @@ }, 5000); fetch("./api/restart", { method: "POST", + headers: { + "Content-Type": "application/json" + } }); }, ddResetPersistence() { this.ddResetPressed = true; - fetch("/api/reset-display-device-persistence", { method: "POST" }) + fetch("/api/reset-display-device-persistence", { + method: "POST", + headers: { + "Content-Type": "application/json" + } + }) .then((r) => r.json()) .then((r) => { this.ddResetPressed = false; diff --git a/src_assets/common/assets/web/welcome.html b/src_assets/common/assets/web/welcome.html index 2f06c8a1..bdacf941 100644 --- a/src_assets/common/assets/web/welcome.html +++ b/src_assets/common/assets/web/welcome.html @@ -78,6 +78,9 @@ this.loading = true; fetch("./api/password", { method: "POST", + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify(this.passwordData), }).then((r) => { this.loading = false;