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 {
|
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
|
Python: `import json
|
||||||
import requests
|
import requests
|
||||||
from requests.auth import HTTPBasicAuth
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
@ -30,6 +30,7 @@ requests.${method.trim().toLowerCase()}(
|
||||||
.then(data => console.log(data));`,
|
.then(data => console.log(data));`,
|
||||||
PowerShell: `Invoke-RestMethod \`
|
PowerShell: `Invoke-RestMethod \`
|
||||||
-SkipCertificateCheck \`
|
-SkipCertificateCheck \`
|
||||||
|
-ContentType 'application/json' \`
|
||||||
-Uri 'https://localhost:47990${endpoint.trim()}' \`
|
-Uri 'https://localhost:47990${endpoint.trim()}' \`
|
||||||
-Method ${method.trim()} \`
|
-Method ${method.trim()} \`
|
||||||
-Headers @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('user:pass'))}
|
-Headers @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('user:pass'))}
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,39 @@ namespace confighttp {
|
||||||
response->write(code, tree.dump(), headers);
|
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.
|
* @brief Get the index page.
|
||||||
* @param response The HTTP response object.
|
* @param response The HTTP response object.
|
||||||
|
|
@ -535,6 +568,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}}
|
* @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}}
|
||||||
*/
|
*/
|
||||||
void saveApp(resp_https_t response, req_https_t request) {
|
void saveApp(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -602,6 +638,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/apps/close| POST| null}
|
* @api_examples{/api/apps/close| POST| null}
|
||||||
*/
|
*/
|
||||||
void closeApp(resp_https_t response, req_https_t request) {
|
void closeApp(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -623,6 +662,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/apps/9999| DELETE| null}
|
* @api_examples{/api/apps/9999| DELETE| null}
|
||||||
*/
|
*/
|
||||||
void deleteApp(resp_https_t response, req_https_t request) {
|
void deleteApp(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -703,6 +745,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/unpair| POST| {"uuid":"1234"}}
|
* @api_examples{/api/unpair| POST| {"uuid":"1234"}}
|
||||||
*/
|
*/
|
||||||
void unpair(resp_https_t response, req_https_t request) {
|
void unpair(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -733,6 +778,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/clients/unpair-all| POST| null}
|
* @api_examples{/api/clients/unpair-all| POST| null}
|
||||||
*/
|
*/
|
||||||
void unpairAll(resp_https_t response, req_https_t request) {
|
void unpairAll(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -809,6 +857,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/config| POST| {"key":"value"}}
|
* @api_examples{/api/config| POST| {"key":"value"}}
|
||||||
*/
|
*/
|
||||||
void saveConfig(resp_https_t response, req_https_t request) {
|
void saveConfig(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
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"}}
|
* @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) {
|
void uploadCover(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -938,6 +992,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/password| POST| {"currentUsername":"admin","currentPassword":"admin","newUsername":"admin","newPassword":"admin","confirmNewPassword":"admin"}}
|
* @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) {
|
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)) {
|
if (!config::sunshine.username.empty() && !authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1008,6 +1065,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}}
|
* @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}}
|
||||||
*/
|
*/
|
||||||
void savePin(resp_https_t response, req_https_t request) {
|
void savePin(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1044,6 +1104,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/reset-display-device-persistence| POST| null}
|
* @api_examples{/api/reset-display-device-persistence| POST| null}
|
||||||
*/
|
*/
|
||||||
void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) {
|
void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1063,6 +1126,9 @@ namespace confighttp {
|
||||||
* @api_examples{/api/restart| POST| null}
|
* @api_examples{/api/restart| POST| null}
|
||||||
*/
|
*/
|
||||||
void restart(resp_https_t response, req_https_t request) {
|
void restart(resp_https_t response, req_https_t request) {
|
||||||
|
if (!check_content_type(response, request, "application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -440,7 +440,12 @@
|
||||||
"Are you sure to delete " + this.apps[id].name + "?"
|
"Are you sure to delete " + this.apps[id].name + "?"
|
||||||
);
|
);
|
||||||
if (resp) {
|
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();
|
if (r.status === 200) document.location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -540,6 +545,9 @@
|
||||||
this.coverFinderBusy = true;
|
this.coverFinderBusy = true;
|
||||||
fetch("./api/covers/upload", {
|
fetch("./api/covers/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
key: cover.key,
|
key: cover.key,
|
||||||
url: cover.saveUrl,
|
url: cover.saveUrl,
|
||||||
|
|
@ -555,6 +563,9 @@
|
||||||
this.editForm["image-path"] = this.editForm["image-path"].toString().replace(/"/g, '');
|
this.editForm["image-path"] = this.editForm["image-path"].toString().replace(/"/g, '');
|
||||||
fetch("./api/apps", {
|
fetch("./api/apps", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: JSON.stringify(this.editForm),
|
body: JSON.stringify(this.editForm),
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
if (r.status === 200) document.location.reload();
|
if (r.status === 200) document.location.reload();
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,9 @@
|
||||||
|
|
||||||
return fetch("./api/config", {
|
return fetch("./api/config", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: JSON.stringify(config),
|
body: JSON.stringify(config),
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
if (r.status === 200) {
|
if (r.status === 200) {
|
||||||
|
|
@ -393,7 +396,10 @@
|
||||||
this.saved = this.restarted = false;
|
this.saved = this.restarted = false;
|
||||||
}, 5000);
|
}, 5000);
|
||||||
fetch("./api/restart", {
|
fetch("./api/restart", {
|
||||||
method: "POST"
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,9 @@
|
||||||
this.error = null;
|
this.error = null;
|
||||||
fetch("./api/password", {
|
fetch("./api/password", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: JSON.stringify(this.passwordData),
|
body: JSON.stringify(this.passwordData),
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
if (r.status === 200) {
|
if (r.status === 200) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,13 @@
|
||||||
let name = document.querySelector("#name-input").value;
|
let name = document.querySelector("#name-input").value;
|
||||||
document.querySelector("#status").innerHTML = "";
|
document.querySelector("#status").innerHTML = "";
|
||||||
let b = JSON.stringify({pin: pin, name: name});
|
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) => response.json())
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === true) {
|
if (response.status === true) {
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,11 @@
|
||||||
},
|
},
|
||||||
closeApp() {
|
closeApp() {
|
||||||
this.closeAppPressed = true;
|
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) => r.json())
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
this.closeAppPressed = false;
|
this.closeAppPressed = false;
|
||||||
|
|
@ -219,7 +223,12 @@
|
||||||
},
|
},
|
||||||
unpairAll() {
|
unpairAll() {
|
||||||
this.unpairAllPressed = true;
|
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) => r.json())
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
this.unpairAllPressed = false;
|
this.unpairAllPressed = false;
|
||||||
|
|
@ -231,7 +240,13 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
unpairSingle(uuid) {
|
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.showApplyMessage = true;
|
||||||
this.refreshClients();
|
this.refreshClients();
|
||||||
});
|
});
|
||||||
|
|
@ -263,11 +278,19 @@
|
||||||
}, 5000);
|
}, 5000);
|
||||||
fetch("./api/restart", {
|
fetch("./api/restart", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
ddResetPersistence() {
|
ddResetPersistence() {
|
||||||
this.ddResetPressed = true;
|
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) => r.json())
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
this.ddResetPressed = false;
|
this.ddResetPressed = false;
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,9 @@
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
fetch("./api/password", {
|
fetch("./api/password", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: JSON.stringify(this.passwordData),
|
body: JSON.stringify(this.passwordData),
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue