Skip to content

Commit 738ac93

Browse files
TheElixZammutoaxflaReenigneArcher
authored
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 <[email protected]> Co-authored-by: ReenigneArcher <[email protected]>
1 parent d6820ba commit 738ac93

File tree

8 files changed

+127
-8
lines changed

8 files changed

+127
-8
lines changed

docs/api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function generateExamples(endpoint, method, body = null) {
99
}
1010

1111
return {
12-
cURL: `curl -u user:pass -X ${method.trim()} -k https://localhost:47990${endpoint.trim()}${curlBodyString}`,
12+
cURL: `curl -u user:pass -H "Content-Type: application/json" -X ${method.trim()} -k https://localhost:47990${endpoint.trim()}${curlBodyString}`,
1313
Python: `import json
1414
import requests
1515
from requests.auth import HTTPBasicAuth
@@ -30,6 +30,7 @@ requests.${method.trim().toLowerCase()}(
3030
.then(data => console.log(data));`,
3131
PowerShell: `Invoke-RestMethod \`
3232
-SkipCertificateCheck \`
33+
-ContentType 'application/json' \`
3334
-Uri 'https://localhost:47990${endpoint.trim()}' \`
3435
-Method ${method.trim()} \`
3536
-Headers @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('user:pass'))}

src/confighttp.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,39 @@ namespace confighttp {
213213
response->write(code, tree.dump(), headers);
214214
}
215215

216+
/**
217+
* @brief Validate the request content type and send bad request when mismatch.
218+
* @param response The HTTP response object.
219+
* @param request The HTTP request object.
220+
* @param contentType The expected content type
221+
*/
222+
bool check_content_type(resp_https_t response, req_https_t request, const std::string_view &contentType) {
223+
auto requestContentType = request->header.find("content-type");
224+
if (requestContentType == request->header.end()) {
225+
bad_request(response, request, "Content type not provided");
226+
return false;
227+
}
228+
// Extract the media type part before any parameters (e.g., charset)
229+
std::string actualContentType = requestContentType->second;
230+
size_t semicolonPos = actualContentType.find(';');
231+
if (semicolonPos != std::string::npos) {
232+
actualContentType = actualContentType.substr(0, semicolonPos);
233+
}
234+
235+
// Trim whitespace and convert to lowercase for case-insensitive comparison
236+
boost::algorithm::trim(actualContentType);
237+
boost::algorithm::to_lower(actualContentType);
238+
239+
std::string expectedContentType(contentType);
240+
boost::algorithm::to_lower(expectedContentType);
241+
242+
if (actualContentType != expectedContentType) {
243+
bad_request(response, request, "Content type mismatch");
244+
return false;
245+
}
246+
return true;
247+
}
248+
216249
/**
217250
* @brief Get the index page.
218251
* @param response The HTTP response object.
@@ -535,6 +568,9 @@ namespace confighttp {
535568
* @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}}
536569
*/
537570
void saveApp(resp_https_t response, req_https_t request) {
571+
if (!check_content_type(response, request, "application/json")) {
572+
return;
573+
}
538574
if (!authenticate(response, request)) {
539575
return;
540576
}
@@ -602,6 +638,9 @@ namespace confighttp {
602638
* @api_examples{/api/apps/close| POST| null}
603639
*/
604640
void closeApp(resp_https_t response, req_https_t request) {
641+
if (!check_content_type(response, request, "application/json")) {
642+
return;
643+
}
605644
if (!authenticate(response, request)) {
606645
return;
607646
}
@@ -623,6 +662,9 @@ namespace confighttp {
623662
* @api_examples{/api/apps/9999| DELETE| null}
624663
*/
625664
void deleteApp(resp_https_t response, req_https_t request) {
665+
if (!check_content_type(response, request, "application/json")) {
666+
return;
667+
}
626668
if (!authenticate(response, request)) {
627669
return;
628670
}
@@ -703,6 +745,9 @@ namespace confighttp {
703745
* @api_examples{/api/unpair| POST| {"uuid":"1234"}}
704746
*/
705747
void unpair(resp_https_t response, req_https_t request) {
748+
if (!check_content_type(response, request, "application/json")) {
749+
return;
750+
}
706751
if (!authenticate(response, request)) {
707752
return;
708753
}
@@ -733,6 +778,9 @@ namespace confighttp {
733778
* @api_examples{/api/clients/unpair-all| POST| null}
734779
*/
735780
void unpairAll(resp_https_t response, req_https_t request) {
781+
if (!check_content_type(response, request, "application/json")) {
782+
return;
783+
}
736784
if (!authenticate(response, request)) {
737785
return;
738786
}
@@ -809,6 +857,9 @@ namespace confighttp {
809857
* @api_examples{/api/config| POST| {"key":"value"}}
810858
*/
811859
void saveConfig(resp_https_t response, req_https_t request) {
860+
if (!check_content_type(response, request, "application/json")) {
861+
return;
862+
}
812863
if (!authenticate(response, request)) {
813864
return;
814865
}
@@ -855,6 +906,9 @@ namespace confighttp {
855906
* @api_examples{/api/covers/upload| POST| {"key":"igdb_1234","url":"https://images.igdb.com/igdb/image/upload/t_cover_big_2x/abc123.png"}}
856907
*/
857908
void uploadCover(resp_https_t response, req_https_t request) {
909+
if (!check_content_type(response, request, "application/json")) {
910+
return;
911+
}
858912
if (!authenticate(response, request)) {
859913
return;
860914
}
@@ -938,6 +992,9 @@ namespace confighttp {
938992
* @api_examples{/api/password| POST| {"currentUsername":"admin","currentPassword":"admin","newUsername":"admin","newPassword":"admin","confirmNewPassword":"admin"}}
939993
*/
940994
void savePassword(resp_https_t response, req_https_t request) {
995+
if (!check_content_type(response, request, "application/json")) {
996+
return;
997+
}
941998
if (!config::sunshine.username.empty() && !authenticate(response, request)) {
942999
return;
9431000
}
@@ -1008,6 +1065,9 @@ namespace confighttp {
10081065
* @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}}
10091066
*/
10101067
void savePin(resp_https_t response, req_https_t request) {
1068+
if (!check_content_type(response, request, "application/json")) {
1069+
return;
1070+
}
10111071
if (!authenticate(response, request)) {
10121072
return;
10131073
}
@@ -1044,6 +1104,9 @@ namespace confighttp {
10441104
* @api_examples{/api/reset-display-device-persistence| POST| null}
10451105
*/
10461106
void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) {
1107+
if (!check_content_type(response, request, "application/json")) {
1108+
return;
1109+
}
10471110
if (!authenticate(response, request)) {
10481111
return;
10491112
}
@@ -1063,6 +1126,9 @@ namespace confighttp {
10631126
* @api_examples{/api/restart| POST| null}
10641127
*/
10651128
void restart(resp_https_t response, req_https_t request) {
1129+
if (!check_content_type(response, request, "application/json")) {
1130+
return;
1131+
}
10661132
if (!authenticate(response, request)) {
10671133
return;
10681134
}

src_assets/common/assets/web/apps.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,12 @@ <h4>{{ $t('apps.env_vars_about') }}</h4>
440440
"Are you sure to delete " + this.apps[id].name + "?"
441441
);
442442
if (resp) {
443-
fetch("./api/apps/" + id, { method: "DELETE" }).then((r) => {
443+
fetch("./api/apps/" + id, {
444+
method: "DELETE",
445+
headers: {
446+
"Content-Type": "application/json"
447+
},
448+
}).then((r) => {
444449
if (r.status === 200) document.location.reload();
445450
});
446451
}
@@ -540,6 +545,9 @@ <h4>{{ $t('apps.env_vars_about') }}</h4>
540545
this.coverFinderBusy = true;
541546
fetch("./api/covers/upload", {
542547
method: "POST",
548+
headers: {
549+
'Content-Type': 'application/json'
550+
},
543551
body: JSON.stringify({
544552
key: cover.key,
545553
url: cover.saveUrl,
@@ -555,6 +563,9 @@ <h4>{{ $t('apps.env_vars_about') }}</h4>
555563
this.editForm["image-path"] = this.editForm["image-path"].toString().replace(/"/g, '');
556564
fetch("./api/apps", {
557565
method: "POST",
566+
headers: {
567+
'Content-Type': 'application/json'
568+
},
558569
body: JSON.stringify(this.editForm),
559570
}).then((r) => {
560571
if (r.status === 200) document.location.reload();

src_assets/common/assets/web/config.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,9 @@ <h1 class="my-4">{{ $t('config.configuration') }}</h1>
371371

372372
return fetch("./api/config", {
373373
method: "POST",
374+
headers: {
375+
'Content-Type': 'application/json'
376+
},
374377
body: JSON.stringify(config),
375378
}).then((r) => {
376379
if (r.status === 200) {
@@ -393,7 +396,10 @@ <h1 class="my-4">{{ $t('config.configuration') }}</h1>
393396
this.saved = this.restarted = false;
394397
}, 5000);
395398
fetch("./api/restart", {
396-
method: "POST"
399+
method: "POST",
400+
headers: {
401+
"Content-Type": "application/json"
402+
}
397403
});
398404
}
399405
});

src_assets/common/assets/web/password.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ <h4>{{ $t('password.new_creds') }}</h4>
9292
this.error = null;
9393
fetch("./api/password", {
9494
method: "POST",
95+
headers: {
96+
'Content-Type': 'application/json'
97+
},
9598
body: JSON.stringify(this.passwordData),
9699
}).then((r) => {
97100
if (r.status === 200) {

src_assets/common/assets/web/pin.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ <h1 class="my-4 text-center">{{ $t('pin.pin_pairing') }}</h1>
3939
let name = document.querySelector("#name-input").value;
4040
document.querySelector("#status").innerHTML = "";
4141
let b = JSON.stringify({pin: pin, name: name});
42-
fetch("./api/pin", {method: "POST", body: b})
42+
fetch("./api/pin", {
43+
method: "POST",
44+
headers: {
45+
'Content-Type': 'application/json'
46+
},
47+
body: b
48+
})
4349
.then((response) => response.json())
4450
.then((response) => {
4551
if (response.status === true) {

src_assets/common/assets/web/troubleshooting.html

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,11 @@ <h2 id="logs">{{ $t('troubleshooting.logs') }}</h2>
207207
},
208208
closeApp() {
209209
this.closeAppPressed = true;
210-
fetch("./api/apps/close", { method: "POST" })
210+
fetch("./api/apps/close", {
211+
method: "POST",
212+
headers: {
213+
"Content-Type": "application/json"
214+
} })
211215
.then((r) => r.json())
212216
.then((r) => {
213217
this.closeAppPressed = false;
@@ -219,7 +223,12 @@ <h2 id="logs">{{ $t('troubleshooting.logs') }}</h2>
219223
},
220224
unpairAll() {
221225
this.unpairAllPressed = true;
222-
fetch("./api/clients/unpair-all", { method: "POST" })
226+
fetch("./api/clients/unpair-all", {
227+
method: "POST",
228+
headers: {
229+
"Content-Type": "application/json"
230+
}
231+
})
223232
.then((r) => r.json())
224233
.then((r) => {
225234
this.unpairAllPressed = false;
@@ -231,7 +240,13 @@ <h2 id="logs">{{ $t('troubleshooting.logs') }}</h2>
231240
});
232241
},
233242
unpairSingle(uuid) {
234-
fetch("./api/clients/unpair", { method: "POST", body: JSON.stringify({ uuid }) }).then(() => {
243+
fetch("./api/clients/unpair", {
244+
method: "POST",
245+
headers: {
246+
'Content-Type': 'application/json'
247+
},
248+
body: JSON.stringify({ uuid })
249+
}).then(() => {
235250
this.showApplyMessage = true;
236251
this.refreshClients();
237252
});
@@ -263,11 +278,19 @@ <h2 id="logs">{{ $t('troubleshooting.logs') }}</h2>
263278
}, 5000);
264279
fetch("./api/restart", {
265280
method: "POST",
281+
headers: {
282+
"Content-Type": "application/json"
283+
}
266284
});
267285
},
268286
ddResetPersistence() {
269287
this.ddResetPressed = true;
270-
fetch("/api/reset-display-device-persistence", { method: "POST" })
288+
fetch("/api/reset-display-device-persistence", {
289+
method: "POST",
290+
headers: {
291+
"Content-Type": "application/json"
292+
}
293+
})
271294
.then((r) => r.json())
272295
.then((r) => {
273296
this.ddResetPressed = false;

src_assets/common/assets/web/welcome.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ <h1 class="mb-0">
7878
this.loading = true;
7979
fetch("./api/password", {
8080
method: "POST",
81+
headers: {
82+
'Content-Type': 'application/json'
83+
},
8184
body: JSON.stringify(this.passwordData),
8285
}).then((r) => {
8386
this.loading = false;

0 commit comments

Comments
 (0)