fix(confighttp): do not return 200 on errors (#3385)
Some checks failed
CI / GitHub Env Debug (push) Has been cancelled
CI / Setup Release (push) Has been cancelled
CI / Setup Flatpak Matrix (push) Has been cancelled
CI Docker / Check Dockerfiles (push) Has been cancelled
CodeQL / Get language matrix (push) Has been cancelled
localize / Update Localization (push) Has been cancelled
Build GH-Pages / prep (push) Has been cancelled
CI / Linux Flatpak (push) Has been cancelled
CI / Linux ${{ matrix.type }} (--appimage-build, 22.04, AppImage) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (macos, 13) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (macos, 14) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (ubuntu, latest) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (ubuntu, latest, true) (push) Has been cancelled
CI / Windows (push) Has been cancelled
CI Docker / Setup Release (push) Has been cancelled
CI Docker / Docker${{ matrix.tag }} (push) Has been cancelled
CodeQL / Analyze (${{ matrix.name }}) (push) Has been cancelled
Build GH-Pages / call-jekyll-build (push) Has been cancelled

Co-authored-by: Lukas Senionis <22381748+FrogTheFrog@users.noreply.github.com>
This commit is contained in:
ReenigneArcher
2025-01-10 15:12:52 -05:00
committed by GitHub
parent 80ecf19d40
commit 40ac718691
4 changed files with 437 additions and 193 deletions

View File

@@ -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<script>(function() { let examples = generateExamples('\1', '\2', \3); document.write(createTabs(examples)); })();</script>@endhtmlonly"

130
docs/api.js Normal file
View File

@@ -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 = '<div class="tabs-overview-container"><div class="tabs-overview">';
let content = '<div class="tab-content">';
languages.forEach((lang, index) => {
const hash = hashString(examples[lang]);
tabs += `<button class="tab-button ${index === 0 ? 'active' : ''}" onclick="openTab(event, '${lang}')"><b class="tab-title" title=" ${lang} "> ${lang} </b></button>`;
content += `<div id="${lang}" class="tabcontent" style="display: ${index === 0 ? 'block' : 'none'};">
<div class="doxygen-awesome-fragment-wrapper">
<div class="fragment">
${examples[lang].split('\n').map(line => `<div class="line">${line}</div>`).join('')}
</div>
<doxygen-awesome-fragment-copy-button id="copy-button-${lang}-${hash}" title="Copy to clipboard">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M0 0h24v24H0V0z" fill="none"></path>
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path>
</svg>
</doxygen-awesome-fragment-copy-button>
</div>
</div>`;
});
tabs += '</div></div>';
content += '</div>';
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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>`;
window.setTimeout(() => {
this.classList.remove("success");
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
}, 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";
}
}

View File

@@ -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
<script src="api.js"></script>
@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()
<div class="section_buttons">

View File

@@ -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.<xmlattr>.status_code", 404);
tree.put("status_code", static_cast<int>(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<int>(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<int>(apps_node.size())) {
std::string error;
if (const int max_index = static_cast<int>(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_<game_id>",
* "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/<slug>.png",
* "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/<slug>.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<std::string>(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<std::string> 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<std::string>("currentUsername") : "";
auto newUsername = inputTree.get<std::string>("newUsername");
@@ -779,15 +883,13 @@ namespace confighttp {
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("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<std::string>("pin");
std::string name = inputTree.get<std::string>("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": "<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<std::string>("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;