Merge commit from fork
* (security) Mandate content-type on POST calls * (security) Add JSON content-type in POST requests with a body * Added Content Type on missing endpoints * (review) docs and newlines * (docs) add JSON content type header * style(clang-format): fix lint errors --------- Co-authored-by: axfla <axfla@hotmail.fr> Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
This commit is contained in:
parent
d6820ba019
commit
738ac93a0e
8 changed files with 127 additions and 8 deletions
|
|
@ -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'))}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue