diff --git a/docs/Doxyfile b/docs/Doxyfile
index 7c08c4f093a..6e95fd0f7a4 100644
--- a/docs/Doxyfile
+++ b/docs/Doxyfile
@@ -62,4 +62,8 @@ INPUT = ../README.md \
HTML_EXTRA_STYLESHEET += doc-styles.css
# extra js
+HTML_EXTRA_FILES += api.js
HTML_EXTRA_FILES += configuration.js
+
+# custom aliases
+ALIASES += api_examples{3|}="@htmlonly@endhtmlonly"
diff --git a/docs/api.js b/docs/api.js
new file mode 100644
index 00000000000..5f887e94e93
--- /dev/null
+++ b/docs/api.js
@@ -0,0 +1,130 @@
+function generateExamples(endpoint, method, body = null) {
+ let curlBodyString = '';
+ let psBodyString = '';
+
+ if (body) {
+ const curlJsonString = JSON.stringify(body).replace(/"/g, '\\"');
+ curlBodyString = ` -d "${curlJsonString}"`;
+ psBodyString = `-Body (ConvertTo-Json ${JSON.stringify(body)})`;
+ }
+
+ return {
+ cURL: `curl -u user:pass -X ${method.trim()} -k https://localhost:47990${endpoint.trim()}${curlBodyString}`,
+ Python: `import json
+import requests
+from requests.auth import HTTPBasicAuth
+
+requests.${method.trim().toLowerCase()}(
+ auth=HTTPBasicAuth('user', 'pass'),
+ url='https://localhost:47990${endpoint.trim()}',
+ verify=False,${body ? `\n json=${JSON.stringify(body)},` : ''}
+).json()`,
+ JavaScript: `fetch('https://localhost:47990${endpoint.trim()}', {
+ method: '${method.trim()}',
+ headers: {
+ 'Authorization': 'Basic ' + btoa('user:pass'),
+ 'Content-Type': 'application/json',
+ }${body ? `,\n body: JSON.stringify(${JSON.stringify(body)}),` : ''}
+})
+.then(response => response.json())
+.then(data => console.log(data));`,
+ PowerShell: `Invoke-RestMethod \`
+ -SkipCertificateCheck \`
+ -Uri 'https://localhost:47990${endpoint.trim()}' \`
+ -Method ${method.trim()} \`
+ -Headers @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('user:pass'))}
+ ${psBodyString}`
+ };
+}
+
+function hashString(str) {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash |= 0; // Convert to 32bit integer
+ }
+ return hash;
+}
+
+function createTabs(examples) {
+ const languages = Object.keys(examples);
+ let tabs = '
';
+ let content = '
';
+
+ languages.forEach((lang, index) => {
+ const hash = hashString(examples[lang]);
+ tabs += `
`;
+ content += `
+
+
+ ${examples[lang].split('\n').map(line => `
${line}
`).join('')}
+
+
+
+
+
+
`;
+ });
+
+ tabs += '
';
+ content += '
';
+
+ setTimeout(() => {
+ languages.forEach((lang, index) => {
+ const hash = hashString(examples[lang]);
+ const copyButton = document.getElementById(`copy-button-${lang}-${hash}`);
+ copyButton.addEventListener('click', copyContent);
+ });
+ }, 0);
+
+ return tabs + content;
+}
+
+function copyContent() {
+ const content = this.previousElementSibling.cloneNode(true);
+ if (content instanceof Element) {
+ // filter out line number from file listings
+ content.querySelectorAll(".lineno, .ttc").forEach((node) => {
+ node.remove();
+ });
+ let textContent = Array.from(content.querySelectorAll('.line'))
+ .map(line => line.innerText)
+ .join('\n')
+ .trim(); // Join lines with newline characters and trim leading/trailing whitespace
+ navigator.clipboard.writeText(textContent);
+ this.classList.add("success");
+ this.innerHTML = ``;
+ window.setTimeout(() => {
+ this.classList.remove("success");
+ this.innerHTML = ``;
+ }, 980);
+ } else {
+ console.error('Failed to copy: content is not a DOM element');
+ }
+}
+
+function openTab(evt, lang) {
+ const tabcontent = document.getElementsByClassName("tabcontent");
+ for (const content of tabcontent) {
+ content.style.display = "none";
+ }
+
+ const tablinks = document.getElementsByClassName("tab-button");
+ for (const link of tablinks) {
+ link.className = link.className.replace(" active", "");
+ }
+
+ const selectedTabs = document.querySelectorAll(`#${lang}`);
+ for (const tab of selectedTabs) {
+ tab.style.display = "block";
+ }
+
+ const selectedButtons = document.querySelectorAll(`.tab-button[onclick*="${lang}"]`);
+ for (const button of selectedButtons) {
+ button.className += " active";
+ }
+}
diff --git a/docs/api.md b/docs/api.md
index 2c6e640989d..e93f500c061 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -5,6 +5,10 @@ Sunshine has a RESTful API which can be used to interact with the service.
Unless otherwise specified, authentication is required for all API calls. You can authenticate using
basic authentication with the admin username and password.
+@htmlonly
+
+@endhtmlonly
+
## GET /api/apps
@copydoc confighttp::getApps()
@@ -14,7 +18,7 @@ basic authentication with the admin username and password.
## POST /api/apps
@copydoc confighttp::saveApp()
-## DELETE /api/apps{index}
+## DELETE /api/apps/{index}
@copydoc confighttp::deleteApp()
## POST /api/covers/upload
@@ -32,6 +36,9 @@ basic authentication with the admin username and password.
## POST /api/restart
@copydoc confighttp::restart()
+## POST /api/reset-display-device-persistence
+@copydoc confighttp::resetDisplayDevicePersistence()
+
## POST /api/password
@copydoc confighttp::savePassword()
@@ -47,7 +54,7 @@ basic authentication with the admin username and password.
## GET /api/clients/list
@copydoc confighttp::listClients()
-## GET /api/apps/close
+## POST /api/apps/close
@copydoc confighttp::closeApp()
diff --git a/src/confighttp.cpp b/src/confighttp.cpp
index 9b8570e0817..f2a36441468 100644
--- a/src/confighttp.cpp
+++ b/src/confighttp.cpp
@@ -58,6 +58,10 @@ namespace confighttp {
REMOVE ///< Remove client
};
+ /**
+ * @brief Log the request details.
+ * @param request The HTTP request object.
+ */
void
print_req(const req_https_t &request) {
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
@@ -76,6 +80,23 @@ namespace confighttp {
BOOST_LOG(debug) << " [--] "sv;
}
+ /**
+ * @brief Send a response.
+ * @param response The HTTP response object.
+ * @param output_tree The JSON tree to send.
+ */
+ void
+ send_response(resp_https_t response, const pt::ptree &output_tree) {
+ std::ostringstream data;
+ pt::write_json(data, output_tree);
+ response->write(data.str());
+ }
+
+ /**
+ * @brief Send a 401 Unauthorized response.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
send_unauthorized(resp_https_t response, req_https_t request) {
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
@@ -86,6 +107,12 @@ namespace confighttp {
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
}
+ /**
+ * @brief Send a redirect response.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ * @param path The path to redirect to.
+ */
void
send_redirect(resp_https_t response, req_https_t request, const char *path) {
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
@@ -96,6 +123,12 @@ namespace confighttp {
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);
}
+ /**
+ * @brief Authenticate the user.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ * @return True if the user is authenticated, false otherwise.
+ */
bool
authenticate(resp_https_t response, req_https_t request) {
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
@@ -142,21 +175,56 @@ namespace confighttp {
return true;
}
+ /**
+ * @brief Send a 404 Not Found response.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
- not_found(resp_https_t response, req_https_t request) {
+ not_found(resp_https_t response, [[maybe_unused]] req_https_t request) {
+ constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found;
+
pt::ptree tree;
- tree.put("root..status_code", 404);
+ tree.put("status_code", static_cast(code));
+ tree.put("error", "Not Found");
std::ostringstream data;
+ pt::write_json(data, tree);
- pt::write_xml(data, tree);
- response->write(data.str());
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "application/json");
- *response << "HTTP/1.1 404 NOT FOUND\r\n"
- << data.str();
+ response->write(code, data.str(), headers);
}
/**
+ * @brief Send a 400 Bad Request response.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ * @param error_message The error message to include in the response.
+ */
+ void
+ bad_request(resp_https_t response, [[maybe_unused]] req_https_t request, const std::string &error_message = "Bad Request") {
+ constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_bad_request;
+
+ pt::ptree tree;
+ tree.put("status_code", static_cast(code));
+ tree.put("status", false);
+ tree.put("error", error_message);
+
+ std::ostringstream data;
+ pt::write_json(data, tree);
+
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "application/json");
+
+ response->write(code, data.str(), headers);
+ }
+
+ /**
+ * @brief Get the index page.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
* @todo combine these functions into a single function that accepts the page, i.e "index", "pin", "apps"
*/
void
@@ -171,6 +239,11 @@ namespace confighttp {
response->write(content, headers);
}
+ /**
+ * @brief Get the PIN page.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
getPinPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
@@ -183,6 +256,11 @@ namespace confighttp {
response->write(content, headers);
}
+ /**
+ * @brief Get the apps page.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
getAppsPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
@@ -196,6 +274,11 @@ namespace confighttp {
response->write(content, headers);
}
+ /**
+ * @brief Get the clients page.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
getClientsPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
@@ -208,6 +291,11 @@ namespace confighttp {
response->write(content, headers);
}
+ /**
+ * @brief Get the configuration page.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
getConfigPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
@@ -220,6 +308,11 @@ namespace confighttp {
response->write(content, headers);
}
+ /**
+ * @brief Get the password page.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
getPasswordPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
@@ -232,6 +325,11 @@ namespace confighttp {
response->write(content, headers);
}
+ /**
+ * @brief Get the welcome page.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
getWelcomePage(resp_https_t response, req_https_t request) {
print_req(request);
@@ -245,6 +343,11 @@ namespace confighttp {
response->write(content, headers);
}
+ /**
+ * @brief Get the troubleshooting page.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
getTroubleshootingPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
@@ -258,6 +361,9 @@ namespace confighttp {
}
/**
+ * @brief Get the favicon image.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
* @todo combine function with getSunshineLogoImage and possibly getNodeModules
* @todo use mime_types map
*/
@@ -272,6 +378,9 @@ namespace confighttp {
}
/**
+ * @brief Get the Sunshine logo image.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
* @todo combine function with getFaviconImage and possibly getNodeModules
* @todo use mime_types map
*/
@@ -285,12 +394,23 @@ namespace confighttp {
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
}
+ /**
+ * @brief Check if a path is a child of another path.
+ * @param base The base path.
+ * @param query The path to check.
+ * @return True if the path is a child of the base path, false otherwise.
+ */
bool
isChildPath(fs::path const &base, fs::path const &query) {
auto relPath = fs::relative(base, query);
return *(relPath.begin()) != fs::path("..");
}
+ /**
+ * @brief Get an asset from the node_modules directory.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ */
void
getNodeModules(resp_https_t response, req_https_t request) {
print_req(request);
@@ -303,32 +423,37 @@ namespace confighttp {
// Don't do anything if file does not exist or is outside the assets directory
if (!isChildPath(filePath, nodeModulesPath)) {
BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the assets folder";
- response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request");
+ bad_request(response, request);
+ return;
}
- else if (!fs::exists(filePath)) {
- response->write(SimpleWeb::StatusCode::client_error_not_found);
+ if (!fs::exists(filePath)) {
+ not_found(response, request);
+ return;
}
- else {
- auto relPath = fs::relative(filePath, webDirPath);
- // get the mime type from the file extension mime_types map
- // remove the leading period from the extension
- auto mimeType = mime_types.find(relPath.extension().string().substr(1));
- // check if the extension is in the map at the x position
- if (mimeType != mime_types.end()) {
- // if it is, set the content type to the mime type
- SimpleWeb::CaseInsensitiveMultimap headers;
- headers.emplace("Content-Type", mimeType->second);
- std::ifstream in(filePath.string(), std::ios::binary);
- response->write(SimpleWeb::StatusCode::success_ok, in, headers);
- }
- // do not return any file if the type is not in the map
+
+ auto relPath = fs::relative(filePath, webDirPath);
+ // get the mime type from the file extension mime_types map
+ // remove the leading period from the extension
+ auto mimeType = mime_types.find(relPath.extension().string().substr(1));
+ // check if the extension is in the map at the x position
+ if (mimeType == mime_types.end()) {
+ bad_request(response, request);
+ return;
}
+
+ // if it is, set the content type to the mime type
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", mimeType->second);
+ std::ifstream in(filePath.string(), std::ios::binary);
+ response->write(SimpleWeb::StatusCode::success_ok, in, headers);
}
/**
* @brief Get the list of available applications.
* @param response The HTTP response object.
* @param request The HTTP request object.
+ *
+ * @api_examples{/api/apps| GET| null}
*/
void
getApps(resp_https_t response, req_https_t request) {
@@ -346,6 +471,8 @@ namespace confighttp {
* @brief Get the logs from the log file.
* @param response The HTTP response object.
* @param request The HTTP request object.
+ *
+ * @api_examples{/api/logs| GET| null}
*/
void
getLogs(resp_https_t response, req_https_t request) {
@@ -360,7 +487,7 @@ namespace confighttp {
}
/**
- * @brief Save an application. If the application already exists, it will be updated, otherwise it will be added.
+ * @brief Save an application. To save a new application the index must be `-1`. To update an existing application, you must provide the current index of the application.
* @param response The HTTP response object.
* @param request The HTTP request object.
* The body for the post request should be JSON serialized in the following format:
@@ -385,9 +512,11 @@ namespace confighttp {
* "detached": [
* "Detached command"
* ],
- * "image-path": "Full path to the application image. Must be a png file.",
+ * "image-path": "Full path to the application image. Must be a png file."
* }
* @endcode
+ *
+ * @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}}
*/
void
saveApp(resp_https_t response, req_https_t request) {
@@ -398,19 +527,12 @@ namespace confighttp {
std::stringstream ss;
ss << request->content.rdbuf();
- pt::ptree outputTree;
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
-
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
- pt::ptree inputTree, fileTree;
-
BOOST_LOG(info) << config::stream.file_apps;
try {
// TODO: Input Validation
+ pt::ptree fileTree;
+ pt::ptree inputTree;
+ pt::ptree outputTree;
pt::read_json(ss, inputTree);
pt::read_json(config::stream.file_apps, fileTree);
@@ -464,23 +586,23 @@ namespace confighttp {
fileTree.add_child("apps", sorted_apps);
pt::write_json(config::stream.file_apps, fileTree);
+ proc::refresh(config::stream.file_apps);
+
+ outputTree.put("status", true);
+ send_response(response, outputTree);
}
catch (std::exception &e) {
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
-
- outputTree.put("status", "false");
- outputTree.put("error", "Invalid Input JSON");
- return;
+ bad_request(response, request, e.what());
}
-
- outputTree.put("status", "true");
- proc::refresh(config::stream.file_apps);
}
/**
* @brief Delete an application.
* @param response The HTTP response object.
* @param request The HTTP request object.
+ *
+ * @api_examples{/api/apps/9999| DELETE| null}
*/
void
deleteApp(resp_https_t response, req_https_t request) {
@@ -489,46 +611,46 @@ namespace confighttp {
print_req(request);
pt::ptree outputTree;
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
-
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
- pt::ptree fileTree;
try {
+ pt::ptree fileTree;
+ pt::ptree newApps;
pt::read_json(config::stream.file_apps, fileTree);
auto &apps_node = fileTree.get_child("apps"s);
int index = stoi(request->path_match[1]);
- if (index < 0) {
- outputTree.put("status", "false");
- outputTree.put("error", "Invalid Index");
+ if (index < 0 || index >= static_cast(apps_node.size())) {
+ std::string error;
+ if (const int max_index = static_cast(apps_node.size()) - 1; max_index < 0) {
+ error = "No applications to delete";
+ }
+ else {
+ error = "'index' out of range, max index is "s + std::to_string(max_index);
+ }
+ bad_request(response, request, error);
return;
}
- else {
- // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
- pt::ptree newApps;
- int i = 0;
- for (const auto &[k, v] : apps_node) {
- if (i++ != index) {
- newApps.push_back(std::make_pair("", v));
- }
+
+ // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
+ int i = 0;
+ for (const auto &[k, v] : apps_node) {
+ if (i++ != index) {
+ newApps.push_back(std::make_pair("", v));
}
- fileTree.erase("apps");
- fileTree.push_back(std::make_pair("apps", newApps));
}
+ fileTree.erase("apps");
+ fileTree.push_back(std::make_pair("apps", newApps));
+
pt::write_json(config::stream.file_apps, fileTree);
+ proc::refresh(config::stream.file_apps);
+
+ outputTree.put("status", true);
+ outputTree.put("result", "application "s + std::to_string(index) + " deleted");
+ send_response(response, outputTree);
}
catch (std::exception &e) {
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
- outputTree.put("status", "false");
- outputTree.put("error", "Invalid File JSON");
- return;
+ bad_request(response, request, e.what());
}
-
- outputTree.put("status", "true");
- proc::refresh(config::stream.file_apps);
}
/**
@@ -539,9 +661,11 @@ namespace confighttp {
* @code{.json}
* {
* "key": "igdb_",
- * "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/.png",
+ * "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/.png"
* }
* @endcode
+ *
+ * @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) {
@@ -551,31 +675,19 @@ namespace confighttp {
std::stringstream configStream;
ss << request->content.rdbuf();
pt::ptree outputTree;
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
-
- SimpleWeb::StatusCode code = SimpleWeb::StatusCode::success_ok;
- if (outputTree.get_child_optional("error").has_value()) {
- code = SimpleWeb::StatusCode::client_error_bad_request;
- }
-
- pt::write_json(data, outputTree);
- response->write(code, data.str());
- });
pt::ptree inputTree;
try {
pt::read_json(ss, inputTree);
}
catch (std::exception &e) {
BOOST_LOG(warning) << "UploadCover: "sv << e.what();
- outputTree.put("status", "false");
- outputTree.put("error", e.what());
+ bad_request(response, request, e.what());
return;
}
auto key = inputTree.get("key", "");
if (key.empty()) {
- outputTree.put("error", "Cover key is required");
+ bad_request(response, request, "Cover key is required");
return;
}
auto url = inputTree.get("url", "");
@@ -586,11 +698,11 @@ namespace confighttp {
std::basic_string path = coverdir + http::url_escape(key) + ".png";
if (!url.empty()) {
if (http::url_get_host(url) != "images.igdb.com") {
- outputTree.put("error", "Only images.igdb.com is allowed");
+ bad_request(response, request, "Only images.igdb.com is allowed");
return;
}
if (!http::download_file(url, path)) {
- outputTree.put("error", "Failed to download cover");
+ bad_request(response, request, "Failed to download cover");
return;
}
}
@@ -600,13 +712,17 @@ namespace confighttp {
std::ofstream imgfile(path);
imgfile.write(data.data(), (int) data.size());
}
+ outputTree.put("status", true);
outputTree.put("path", path);
+ send_response(response, outputTree);
}
/**
* @brief Get the configuration settings.
* @param response The HTTP response object.
* @param request The HTTP request object.
+ *
+ * @api_examples{/api/config| GET| null}
*/
void
getConfig(resp_https_t response, req_https_t request) {
@@ -615,14 +731,7 @@ namespace confighttp {
print_req(request);
pt::ptree outputTree;
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
-
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
- outputTree.put("status", "true");
+ outputTree.put("status", true);
outputTree.put("platform", SUNSHINE_PLATFORM);
outputTree.put("version", PROJECT_VER);
@@ -631,12 +740,16 @@ namespace confighttp {
for (auto &[name, value] : vars) {
outputTree.put(std::move(name), std::move(value));
}
+
+ send_response(response, outputTree);
}
/**
* @brief Get the locale setting. This endpoint does not require authentication.
* @param response The HTTP response object.
* @param request The HTTP request object.
+ *
+ * @api_examples{/api/configLocale| GET| null}
*/
void
getLocale(resp_https_t response, req_https_t request) {
@@ -645,15 +758,9 @@ namespace confighttp {
print_req(request);
pt::ptree outputTree;
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
-
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
- outputTree.put("status", "true");
+ outputTree.put("status", true);
outputTree.put("locale", config::sunshine.locale);
+ send_response(response, outputTree);
}
/**
@@ -668,6 +775,8 @@ namespace confighttp {
* @endcode
*
* @attention{It is recommended to ONLY save the config settings that differ from the default behavior.}
+ *
+ * @api_examples{/api/config| POST| {"key":"value"}}
*/
void
saveConfig(resp_https_t response, req_https_t request) {
@@ -678,16 +787,10 @@ namespace confighttp {
std::stringstream ss;
std::stringstream configStream;
ss << request->content.rdbuf();
- pt::ptree outputTree;
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
-
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
- pt::ptree inputTree;
try {
// TODO: Input Validation
+ pt::ptree inputTree;
+ pt::ptree outputTree;
pt::read_json(ss, inputTree);
for (const auto &[k, v] : inputTree) {
std::string value = inputTree.get(k);
@@ -696,12 +799,12 @@ namespace confighttp {
configStream << k << " = " << value << std::endl;
}
file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str());
+ outputTree.put("status", true);
+ send_response(response, outputTree);
}
catch (std::exception &e) {
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
- outputTree.put("status", "false");
- outputTree.put("error", e.what());
- return;
+ bad_request(response, request, e.what());
}
}
@@ -709,6 +812,8 @@ namespace confighttp {
* @brief Restart Sunshine.
* @param response The HTTP response object.
* @param request The HTTP request object.
+ *
+ * @api_examples{/api/restart| POST| null}
*/
void
restart(resp_https_t response, req_https_t request) {
@@ -720,6 +825,24 @@ namespace confighttp {
platf::restart();
}
+ /**
+ * @brief Reset the display device persistence.
+ * @param response The HTTP response object.
+ * @param request The HTTP request object.
+ *
+ * @api_examples{/api/reset-display-device-persistence| POST| null}
+ */
+ void
+ resetDisplayDevicePersistence(resp_https_t response, req_https_t request) {
+ if (!authenticate(response, request)) return;
+
+ print_req(request);
+
+ pt::ptree outputTree;
+ outputTree.put("status", display_device::reset_persistence());
+ send_response(response, outputTree);
+ }
+
/**
* @brief Update existing credentials.
* @param response The HTTP response object.
@@ -734,43 +857,24 @@ namespace confighttp {
* "confirmNewPassword": "Confirm New Password"
* }
* @endcode
+ *
+ * @api_examples{/api/password| POST| {"currentUsername":"admin","currentPassword":"admin","newUsername":"admin","newPassword":"admin","confirmNewPassword":"admin"}}
*/
- void
- resetDisplayDevicePersistence(resp_https_t response, req_https_t request) {
- if (!authenticate(response, request)) return;
-
- print_req(request);
-
- pt::ptree outputTree;
- auto g = util::fail_guard([&outputTree, &response]() {
- std::ostringstream data;
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
- outputTree.put("status", display_device::reset_persistence());
- }
-
void
savePassword(resp_https_t response, req_https_t request) {
if (!config::sunshine.username.empty() && !authenticate(response, request)) return;
print_req(request);
+ std::vector errors = {};
std::stringstream ss;
std::stringstream configStream;
ss << request->content.rdbuf();
- pt::ptree inputTree, outputTree;
-
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
try {
// TODO: Input Validation
+ pt::ptree inputTree;
+ pt::ptree outputTree;
pt::read_json(ss, inputTree);
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get("currentUsername") : "";
auto newUsername = inputTree.get("newUsername");
@@ -779,15 +883,13 @@ namespace confighttp {
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get("confirmNewPassword") : "";
if (newUsername.length() == 0) newUsername = username;
if (newUsername.length() == 0) {
- outputTree.put("status", false);
- outputTree.put("error", "Invalid Username");
+ errors.emplace_back("Invalid Username");
}
else {
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
if (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) {
if (newPassword.empty() || newPassword != confirmPassword) {
- outputTree.put("status", false);
- outputTree.put("error", "Password Mismatch");
+ errors.emplace_back("Password Mismatch");
}
else {
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
@@ -796,16 +898,25 @@ namespace confighttp {
}
}
else {
- outputTree.put("status", false);
- outputTree.put("error", "Invalid Current Credentials");
+ errors.emplace_back("Invalid Current Credentials");
}
}
+
+ if (!errors.empty()) {
+ // join the errors array
+ std::string error = std::accumulate(errors.begin(), errors.end(), std::string(),
+ [](const std::string &a, const std::string &b) {
+ return a.empty() ? b : a + ", " + b;
+ });
+ bad_request(response, request, error);
+ return;
+ }
+
+ send_response(response, outputTree);
}
catch (std::exception &e) {
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
- outputTree.put("status", false);
- outputTree.put("error", e.what());
- return;
+ bad_request(response, request, e.what());
}
}
@@ -820,6 +931,8 @@ namespace confighttp {
* "name": "Friendly Client Name"
* }
* @endcode
+ *
+ * @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}}
*/
void
savePin(resp_https_t response, req_https_t request) {
@@ -830,26 +943,19 @@ namespace confighttp {
std::stringstream ss;
ss << request->content.rdbuf();
- pt::ptree inputTree, outputTree;
-
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
try {
// TODO: Input Validation
+ pt::ptree inputTree;
+ pt::ptree outputTree;
pt::read_json(ss, inputTree);
std::string pin = inputTree.get("pin");
std::string name = inputTree.get("name");
outputTree.put("status", nvhttp::pin(pin, name));
+ send_response(response, outputTree);
}
catch (std::exception &e) {
BOOST_LOG(warning) << "SavePin: "sv << e.what();
- outputTree.put("status", false);
- outputTree.put("error", e.what());
- return;
+ bad_request(response, request, e.what());
}
}
@@ -857,6 +963,8 @@ namespace confighttp {
* @brief Unpair all clients.
* @param response The HTTP response object.
* @param request The HTTP request object.
+ *
+ * @api_examples{/api/clients/unpair-all| POST| null}
*/
void
unpairAll(resp_https_t response, req_https_t request) {
@@ -864,16 +972,12 @@ namespace confighttp {
print_req(request);
- pt::ptree outputTree;
-
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
nvhttp::erase_all_clients();
proc::proc.terminate();
+
+ pt::ptree outputTree;
outputTree.put("status", true);
+ send_response(response, outputTree);
}
/**
@@ -886,6 +990,8 @@ namespace confighttp {
* "uuid": ""
* }
* @endcode
+ *
+ * @api_examples{/api/unpair| POST| {"uuid":"1234"}}
*/
void
unpair(resp_https_t response, req_https_t request) {
@@ -896,25 +1002,18 @@ namespace confighttp {
std::stringstream ss;
ss << request->content.rdbuf();
- pt::ptree inputTree, outputTree;
-
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
try {
// TODO: Input Validation
+ pt::ptree inputTree;
+ pt::ptree outputTree;
pt::read_json(ss, inputTree);
std::string uuid = inputTree.get("uuid");
outputTree.put("status", nvhttp::unpair_client(uuid));
+ send_response(response, outputTree);
}
catch (std::exception &e) {
BOOST_LOG(warning) << "Unpair: "sv << e.what();
- outputTree.put("status", false);
- outputTree.put("error", e.what());
- return;
+ bad_request(response, request, e.what());
}
}
@@ -922,6 +1021,8 @@ namespace confighttp {
* @brief Get the list of paired clients.
* @param response The HTTP response object.
* @param request The HTTP request object.
+ *
+ * @api_examples{/api/clients/list| GET| null}
*/
void
listClients(resp_https_t response, req_https_t request) {
@@ -929,26 +1030,21 @@ namespace confighttp {
print_req(request);
- pt::ptree named_certs = nvhttp::get_all_clients();
+ const pt::ptree named_certs = nvhttp::get_all_clients();
pt::ptree outputTree;
-
outputTree.put("status", false);
-
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
outputTree.add_child("named_certs", named_certs);
outputTree.put("status", true);
+ send_response(response, outputTree);
}
/**
* @brief Close the currently running application.
* @param response The HTTP response object.
* @param request The HTTP request object.
+ *
+ * @api_examples{/api/apps/close| POST| null}
*/
void
closeApp(resp_https_t response, req_https_t request) {
@@ -956,16 +1052,11 @@ namespace confighttp {
print_req(request);
- pt::ptree outputTree;
-
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
proc::proc.terminate();
+
+ pt::ptree outputTree;
outputTree.put("status", true);
+ send_response(response, outputTree);
}
void
@@ -976,6 +1067,18 @@ namespace confighttp {
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
https_server_t server { config::nvhttp.cert, config::nvhttp.pkey };
+ server.default_resource["DELETE"] = [](resp_https_t response, req_https_t request) {
+ bad_request(response, request);
+ };
+ server.default_resource["PATCH"] = [](resp_https_t response, req_https_t request) {
+ bad_request(response, request);
+ };
+ server.default_resource["POST"] = [](resp_https_t response, req_https_t request) {
+ bad_request(response, request);
+ };
+ server.default_resource["PUT"] = [](resp_https_t response, req_https_t request) {
+ bad_request(response, request);
+ };
server.default_resource["GET"] = not_found;
server.resource["^/$"]["GET"] = getIndexPage;
server.resource["^/pin/?$"]["GET"] = getPinPage;