Validate application/json on server side

This commit is contained in:
Yukino Song
2025-05-28 18:01:48 +08:00
parent f3d2078b11
commit a3e0318794
3 changed files with 124 additions and 72 deletions

View File

@@ -238,6 +238,28 @@ 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 required content type.
*/
bool validateContentType(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;
}
if (requestContentType->second != contentType) {
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.
@@ -561,7 +583,7 @@ namespace confighttp {
* @api_examples{/api/apps| POST| {"name":"Hello, World!","uuid": "aaaa-bbbb"}} * @api_examples{/api/apps| POST| {"name":"Hello, World!","uuid": "aaaa-bbbb"}}
*/ */
void saveApp(resp_https_t response, req_https_t request) { void saveApp(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
@@ -627,7 +649,7 @@ namespace confighttp {
* @api_examples{/api/apps/reorder| POST| {"order": ["aaaa-bbbb", "cccc-dddd"]}} * @api_examples{/api/apps/reorder| POST| {"order": ["aaaa-bbbb", "cccc-dddd"]}}
*/ */
void reorderApps(resp_https_t response, req_https_t request) { void reorderApps(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
@@ -738,22 +760,27 @@ namespace confighttp {
* @param response The HTTP response object. * @param response The HTTP response object.
* @param request The HTTP request object. * @param request The HTTP request object.
* *
* @api_examples{/api/apps/9999| DELETE| null} * @api_examples{/api/apps/delete | POST| { uuid: 'aaaa-bbbb' }}
*/ */
void deleteApp(resp_https_t response, req_https_t request) { void deleteApp(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
}
print_req(request); print_req(request);
auto args = request->parse_query_string();
if (args.find("uuid"s) == std::end(args)) {
bad_request(response, request, "Missing a required parameter to delete app");
return;
}
auto uuid = nvhttp::get_arg(args, "uuid");
try { try {
std::stringstream ss;
ss << request->content.rdbuf();
nlohmann::json input_tree = nlohmann::json::parse(ss.str());
// Check for required uuid field in body
if (!input_tree.contains("uuid") || !input_tree["uuid"].is_string()) {
bad_request(response, request, "Missing or invalid uuid in request body");
return;
}
auto uuid = input_tree["uuid"].get<std::string>();
// Read the apps file into a nlohmann::json object. // Read the apps file into a nlohmann::json object.
std::string content = file_handler::read_file(config::stream.file_apps.c_str()); std::string content = file_handler::read_file(config::stream.file_apps.c_str());
nlohmann::json fileTree = nlohmann::json::parse(content); nlohmann::json fileTree = nlohmann::json::parse(content);
@@ -826,7 +853,7 @@ namespace confighttp {
* @endcode * @endcode
*/ */
void updateClient(resp_https_t response, req_https_t request) { void updateClient(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
@@ -866,7 +893,7 @@ namespace confighttp {
* @api_examples{/api/clients/unpair| POST| {"uuid":"1234"}} * @api_examples{/api/clients/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 (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
@@ -965,7 +992,7 @@ 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 (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
@@ -1004,7 +1031,7 @@ 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 (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
@@ -1089,7 +1116,7 @@ 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 (!config::sunshine.username.empty() && !authenticate(response, request)) if ((!config::sunshine.username.empty() && !authenticate(response, request)) || !validateContentType(response, request, "application/json"sv))
return; return;
print_req(request); print_req(request);
std::vector<std::string> errors; std::vector<std::string> errors;
@@ -1146,7 +1173,7 @@ namespace confighttp {
* @api_examples{/api/otp| GET| null} * @api_examples{/api/otp| GET| null}
*/ */
void getOTP(resp_https_t response, req_https_t request) { void getOTP(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
@@ -1154,17 +1181,17 @@ namespace confighttp {
nlohmann::json output_tree; nlohmann::json output_tree;
try { try {
auto args = request->parse_query_string(); std::stringstream ss;
auto it = args.find("passphrase"); ss << request->content.rdbuf();
if (it == args.end()) nlohmann::json input_tree = nlohmann::json::parse(ss.str());
std::string passphrase = input_tree.value("passphrase", "");
if (passphrase.empty())
throw std::runtime_error("Passphrase not provided!"); throw std::runtime_error("Passphrase not provided!");
if (it->second.size() < 4) if (passphrase.size() < 4)
throw std::runtime_error("Passphrase too short!"); throw std::runtime_error("Passphrase too short!");
std::string passphrase = it->second;
std::string deviceName; std::string deviceName = input_tree.value("deviceName", "");
it = args.find("deviceName");
if (it != args.end())
deviceName = it->second;
output_tree["otp"] = nvhttp::request_otp(passphrase, deviceName); output_tree["otp"] = nvhttp::request_otp(passphrase, deviceName);
output_tree["ip"] = platf::get_local_ip_for_gateway(); output_tree["ip"] = platf::get_local_ip_for_gateway();
output_tree["name"] = config::nvhttp.sunshine_name; output_tree["name"] = config::nvhttp.sunshine_name;
@@ -1193,15 +1220,15 @@ 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 (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
print_req(request); print_req(request);
std::stringstream ss;
ss << request->content.rdbuf();
try { try {
std::stringstream ss;
ss << request->content.rdbuf();
nlohmann::json input_tree = nlohmann::json::parse(ss.str()); nlohmann::json input_tree = nlohmann::json::parse(ss.str());
nlohmann::json output_tree; nlohmann::json output_tree;
std::string pin = input_tree.value("pin", ""); std::string pin = input_tree.value("pin", "");
@@ -1288,43 +1315,54 @@ namespace confighttp {
* @param request The HTTP request object. * @param request The HTTP request object.
*/ */
void launchApp(resp_https_t response, req_https_t request) { void launchApp(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
print_req(request); print_req(request);
nlohmann::json output_tree; try {
auto args = request->parse_query_string(); std::stringstream ss;
if (args.find("uuid") == args.end()) { ss << request->content.rdbuf();
bad_request(response, request, "Missing a required launch parameter"); nlohmann::json input_tree = nlohmann::json::parse(ss.str());
return;
} // Check for required uuid field in body
std::string uuid = nvhttp::get_arg(args, "uuid"); if (!input_tree.contains("uuid") || !input_tree["uuid"].is_string()) {
const auto &apps = proc::proc.get_apps(); bad_request(response, request, "Missing or invalid uuid in request body");
for (auto &app : apps) {
if (app.uuid == uuid) {
crypto::named_cert_t named_cert {
.name = "",
.uuid = http::unique_id,
.perm = crypto::PERM::_all,
};
BOOST_LOG(info) << "Launching app ["sv << app.name << "] from web UI"sv;
auto launch_session = nvhttp::make_launch_session(true, false, args, &named_cert);
auto err = proc::proc.execute(app, launch_session);
if (err) {
bad_request(response, request, err == 503 ?
"Failed to initialize video capture/encoding. Is a display connected and turned on?" :
"Failed to start the specified application");
} else {
output_tree["status"] = true;
send_response(response, output_tree);
}
return; return;
} }
std::string uuid = input_tree["uuid"].get<std::string>();
nlohmann::json output_tree;
const auto &apps = proc::proc.get_apps();
for (auto &app : apps) {
if (app.uuid == uuid) {
crypto::named_cert_t named_cert {
.name = "",
.uuid = http::unique_id,
.perm = crypto::PERM::_all,
};
BOOST_LOG(info) << "Launching app ["sv << app.name << "] from web UI"sv;
auto launch_session = nvhttp::make_launch_session(true, false, request->parse_query_string(), &named_cert);
auto err = proc::proc.execute(app, launch_session);
if (err) {
bad_request(response, request, err == 503 ?
"Failed to initialize video capture/encoding. Is a display connected and turned on?" :
"Failed to start the specified application");
} else {
output_tree["status"] = true;
send_response(response, output_tree);
}
return;
}
}
BOOST_LOG(error) << "Couldn't find app with uuid ["sv << uuid << ']';
bad_request(response, request, "Cannot find requested application");
}
catch (std::exception &e) {
BOOST_LOG(warning) << "LaunchApp: "sv << e.what();
bad_request(response, request, e.what());
} }
BOOST_LOG(error) << "Couldn't find app with uuid ["sv << uuid << ']';
bad_request(response, request, "Cannot find requested application");
} }
/** /**
@@ -1333,24 +1371,24 @@ namespace confighttp {
* @param request The HTTP request object. * @param request The HTTP request object.
*/ */
void disconnect(resp_https_t response, req_https_t request) { void disconnect(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) { if (!authenticate(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
print_req(request); print_req(request);
std::stringstream ss;
ss << request->content.rdbuf();
nlohmann::json output_tree;
try { try {
std::stringstream ss;
ss << request->content.rdbuf();
nlohmann::json output_tree;
nlohmann::json input_tree = nlohmann::json::parse(ss.str()); nlohmann::json input_tree = nlohmann::json::parse(ss.str());
std::string uuid = input_tree.value("uuid", ""); std::string uuid = input_tree.value("uuid", "");
output_tree["status"] = nvhttp::find_and_stop_session(uuid, true); output_tree["status"] = nvhttp::find_and_stop_session(uuid, true);
send_response(response, output_tree);
} catch (std::exception &e) { } catch (std::exception &e) {
BOOST_LOG(warning) << "Disconnect: "sv << e.what(); BOOST_LOG(warning) << "Disconnect: "sv << e.what();
bad_request(response, request, e.what()); bad_request(response, request, e.what());
} }
send_response(response, output_tree);
} }
/** /**
@@ -1367,16 +1405,17 @@ namespace confighttp {
* @endcode * @endcode
*/ */
void login(resp_https_t response, req_https_t request) { void login(resp_https_t response, req_https_t request) {
if (!checkIPOrigin(response, request)) { if (!checkIPOrigin(response, request) || !validateContentType(response, request, "application/json"sv)) {
return; return;
} }
auto fg = util::fail_guard([&]{ auto fg = util::fail_guard([&]{
response->write(SimpleWeb::StatusCode::client_error_unauthorized); response->write(SimpleWeb::StatusCode::client_error_unauthorized);
}); });
std::stringstream ss;
ss << request->content.rdbuf();
try { try {
std::stringstream ss;
ss << request->content.rdbuf();
nlohmann::json input_tree = nlohmann::json::parse(ss.str()); nlohmann::json input_tree = nlohmann::json::parse(ss.str());
std::string username = input_tree.value("username", ""); std::string username = input_tree.value("username", "");
std::string password = input_tree.value("password", ""); std::string password = input_tree.value("password", "");
@@ -1431,7 +1470,7 @@ namespace confighttp {
server.resource["^/troubleshooting/?$"]["GET"] = getTroubleshootingPage; server.resource["^/troubleshooting/?$"]["GET"] = getTroubleshootingPage;
server.resource["^/api/login"]["POST"] = login; server.resource["^/api/login"]["POST"] = login;
server.resource["^/api/pin$"]["POST"] = savePin; server.resource["^/api/pin$"]["POST"] = savePin;
server.resource["^/api/otp$"]["GET"] = getOTP; server.resource["^/api/otp$"]["POST"] = getOTP;
server.resource["^/api/apps$"]["GET"] = getApps; server.resource["^/api/apps$"]["GET"] = getApps;
server.resource["^/api/apps$"]["POST"] = saveApp; server.resource["^/api/apps$"]["POST"] = saveApp;
server.resource["^/api/apps/reorder$"]["POST"] = reorderApps; server.resource["^/api/apps/reorder$"]["POST"] = reorderApps;

View File

@@ -637,9 +637,13 @@
launchApp(app) { launchApp(app) {
if (confirm(this.$t('apps.launch_warning'))) { if (confirm(this.$t('apps.launch_warning'))) {
this.actionDisabled = true; this.actionDisabled = true;
fetch("./api/apps/launch?uuid=" + app.uuid, { fetch("./api/apps/launch", {
credentials: 'include', credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
method: 'POST', method: 'POST',
body: JSON.stringify({ uuid: app.uuid })
}) })
.then(r => r.json()) .then(r => r.json())
.then(r => { .then(r => {
@@ -682,9 +686,13 @@
); );
if (resp) { if (resp) {
this.actionDisabled = true; this.actionDisabled = true;
fetch("./api/apps/delete?uuid=" + app.uuid, { fetch("./api/apps/delete", {
credentials: 'include', credentials: 'include',
method: 'POST' headers: {
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify({ uuid: app.uuid })
}).then((r) => r.json()) }).then((r) => r.json())
.then((r) => { .then((r) => {
if (!r.status) { if (!r.status) {

View File

@@ -421,8 +421,13 @@
requestOTP() { requestOTP() {
if (this.editingHost) return; if (this.editingHost) return;
fetch(`./api/otp?passphrase=${this.passphrase}${this.deviceName && `&deviceName=${this.deviceName}` || ''}`, { fetch("./api/otp", {
credentials: 'include' credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify({ passphrase: this.passphrase, deviceName: this.deviceName })
}) })
.then(resp => resp.json()) .then(resp => resp.json())
.then(resp => { .then(resp => {