Validate application/json on server side
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user