fix(confighttp): do not return 200 on errors (#3385)
Co-authored-by: Lukas Senionis <22381748+FrogTheFrog@users.noreply.github.com>
This commit is contained in:
parent
80ecf19d40
commit
40ac718691
4 changed files with 437 additions and 193 deletions
|
|
@ -62,4 +62,8 @@ INPUT = ../README.md \
|
||||||
HTML_EXTRA_STYLESHEET += doc-styles.css
|
HTML_EXTRA_STYLESHEET += doc-styles.css
|
||||||
|
|
||||||
# extra js
|
# extra js
|
||||||
|
HTML_EXTRA_FILES += api.js
|
||||||
HTML_EXTRA_FILES += configuration.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
130
docs/api.js
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
11
docs/api.md
11
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
|
Unless otherwise specified, authentication is required for all API calls. You can authenticate using
|
||||||
basic authentication with the admin username and password.
|
basic authentication with the admin username and password.
|
||||||
|
|
||||||
|
@htmlonly
|
||||||
|
<script src="api.js"></script>
|
||||||
|
@endhtmlonly
|
||||||
|
|
||||||
## GET /api/apps
|
## GET /api/apps
|
||||||
@copydoc confighttp::getApps()
|
@copydoc confighttp::getApps()
|
||||||
|
|
||||||
|
|
@ -14,7 +18,7 @@ basic authentication with the admin username and password.
|
||||||
## POST /api/apps
|
## POST /api/apps
|
||||||
@copydoc confighttp::saveApp()
|
@copydoc confighttp::saveApp()
|
||||||
|
|
||||||
## DELETE /api/apps{index}
|
## DELETE /api/apps/{index}
|
||||||
@copydoc confighttp::deleteApp()
|
@copydoc confighttp::deleteApp()
|
||||||
|
|
||||||
## POST /api/covers/upload
|
## POST /api/covers/upload
|
||||||
|
|
@ -32,6 +36,9 @@ basic authentication with the admin username and password.
|
||||||
## POST /api/restart
|
## POST /api/restart
|
||||||
@copydoc confighttp::restart()
|
@copydoc confighttp::restart()
|
||||||
|
|
||||||
|
## POST /api/reset-display-device-persistence
|
||||||
|
@copydoc confighttp::resetDisplayDevicePersistence()
|
||||||
|
|
||||||
## POST /api/password
|
## POST /api/password
|
||||||
@copydoc confighttp::savePassword()
|
@copydoc confighttp::savePassword()
|
||||||
|
|
||||||
|
|
@ -47,7 +54,7 @@ basic authentication with the admin username and password.
|
||||||
## GET /api/clients/list
|
## GET /api/clients/list
|
||||||
@copydoc confighttp::listClients()
|
@copydoc confighttp::listClients()
|
||||||
|
|
||||||
## GET /api/apps/close
|
## POST /api/apps/close
|
||||||
@copydoc confighttp::closeApp()
|
@copydoc confighttp::closeApp()
|
||||||
|
|
||||||
<div class="section_buttons">
|
<div class="section_buttons">
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ namespace confighttp {
|
||||||
REMOVE ///< Remove client
|
REMOVE ///< Remove client
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Log the request details.
|
||||||
|
* @param request The HTTP request object.
|
||||||
|
*/
|
||||||
void
|
void
|
||||||
print_req(const req_https_t &request) {
|
print_req(const req_https_t &request) {
|
||||||
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
||||||
|
|
@ -76,6 +80,23 @@ namespace confighttp {
|
||||||
BOOST_LOG(debug) << " [--] "sv;
|
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
|
void
|
||||||
send_unauthorized(resp_https_t response, req_https_t request) {
|
send_unauthorized(resp_https_t response, req_https_t request) {
|
||||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
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);
|
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
|
void
|
||||||
send_redirect(resp_https_t response, req_https_t request, const char *path) {
|
send_redirect(resp_https_t response, req_https_t request, const char *path) {
|
||||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
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);
|
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
|
bool
|
||||||
authenticate(resp_https_t response, req_https_t request) {
|
authenticate(resp_https_t response, req_https_t request) {
|
||||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||||
|
|
@ -142,21 +175,56 @@ namespace confighttp {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a 404 Not Found response.
|
||||||
|
* @param response The HTTP response object.
|
||||||
|
* @param request The HTTP request object.
|
||||||
|
*/
|
||||||
void
|
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;
|
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;
|
std::ostringstream data;
|
||||||
|
pt::write_json(data, tree);
|
||||||
|
|
||||||
pt::write_xml(data, tree);
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||||
response->write(data.str());
|
headers.emplace("Content-Type", "application/json");
|
||||||
|
|
||||||
*response << "HTTP/1.1 404 NOT FOUND\r\n"
|
response->write(code, data.str(), headers);
|
||||||
<< data.str();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @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"
|
* @todo combine these functions into a single function that accepts the page, i.e "index", "pin", "apps"
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
|
|
@ -171,6 +239,11 @@ namespace confighttp {
|
||||||
response->write(content, headers);
|
response->write(content, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the PIN page.
|
||||||
|
* @param response The HTTP response object.
|
||||||
|
* @param request The HTTP request object.
|
||||||
|
*/
|
||||||
void
|
void
|
||||||
getPinPage(resp_https_t response, req_https_t request) {
|
getPinPage(resp_https_t response, req_https_t request) {
|
||||||
if (!authenticate(response, request)) return;
|
if (!authenticate(response, request)) return;
|
||||||
|
|
@ -183,6 +256,11 @@ namespace confighttp {
|
||||||
response->write(content, headers);
|
response->write(content, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the apps page.
|
||||||
|
* @param response The HTTP response object.
|
||||||
|
* @param request The HTTP request object.
|
||||||
|
*/
|
||||||
void
|
void
|
||||||
getAppsPage(resp_https_t response, req_https_t request) {
|
getAppsPage(resp_https_t response, req_https_t request) {
|
||||||
if (!authenticate(response, request)) return;
|
if (!authenticate(response, request)) return;
|
||||||
|
|
@ -196,6 +274,11 @@ namespace confighttp {
|
||||||
response->write(content, headers);
|
response->write(content, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the clients page.
|
||||||
|
* @param response The HTTP response object.
|
||||||
|
* @param request The HTTP request object.
|
||||||
|
*/
|
||||||
void
|
void
|
||||||
getClientsPage(resp_https_t response, req_https_t request) {
|
getClientsPage(resp_https_t response, req_https_t request) {
|
||||||
if (!authenticate(response, request)) return;
|
if (!authenticate(response, request)) return;
|
||||||
|
|
@ -208,6 +291,11 @@ namespace confighttp {
|
||||||
response->write(content, headers);
|
response->write(content, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the configuration page.
|
||||||
|
* @param response The HTTP response object.
|
||||||
|
* @param request The HTTP request object.
|
||||||
|
*/
|
||||||
void
|
void
|
||||||
getConfigPage(resp_https_t response, req_https_t request) {
|
getConfigPage(resp_https_t response, req_https_t request) {
|
||||||
if (!authenticate(response, request)) return;
|
if (!authenticate(response, request)) return;
|
||||||
|
|
@ -220,6 +308,11 @@ namespace confighttp {
|
||||||
response->write(content, headers);
|
response->write(content, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the password page.
|
||||||
|
* @param response The HTTP response object.
|
||||||
|
* @param request The HTTP request object.
|
||||||
|
*/
|
||||||
void
|
void
|
||||||
getPasswordPage(resp_https_t response, req_https_t request) {
|
getPasswordPage(resp_https_t response, req_https_t request) {
|
||||||
if (!authenticate(response, request)) return;
|
if (!authenticate(response, request)) return;
|
||||||
|
|
@ -232,6 +325,11 @@ namespace confighttp {
|
||||||
response->write(content, headers);
|
response->write(content, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the welcome page.
|
||||||
|
* @param response The HTTP response object.
|
||||||
|
* @param request The HTTP request object.
|
||||||
|
*/
|
||||||
void
|
void
|
||||||
getWelcomePage(resp_https_t response, req_https_t request) {
|
getWelcomePage(resp_https_t response, req_https_t request) {
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
@ -245,6 +343,11 @@ namespace confighttp {
|
||||||
response->write(content, headers);
|
response->write(content, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the troubleshooting page.
|
||||||
|
* @param response The HTTP response object.
|
||||||
|
* @param request The HTTP request object.
|
||||||
|
*/
|
||||||
void
|
void
|
||||||
getTroubleshootingPage(resp_https_t response, req_https_t request) {
|
getTroubleshootingPage(resp_https_t response, req_https_t request) {
|
||||||
if (!authenticate(response, request)) return;
|
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 combine function with getSunshineLogoImage and possibly getNodeModules
|
||||||
* @todo use mime_types map
|
* @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 combine function with getFaviconImage and possibly getNodeModules
|
||||||
* @todo use mime_types map
|
* @todo use mime_types map
|
||||||
*/
|
*/
|
||||||
|
|
@ -285,12 +394,23 @@ namespace confighttp {
|
||||||
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
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
|
bool
|
||||||
isChildPath(fs::path const &base, fs::path const &query) {
|
isChildPath(fs::path const &base, fs::path const &query) {
|
||||||
auto relPath = fs::relative(base, query);
|
auto relPath = fs::relative(base, query);
|
||||||
return *(relPath.begin()) != fs::path("..");
|
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
|
void
|
||||||
getNodeModules(resp_https_t response, req_https_t request) {
|
getNodeModules(resp_https_t response, req_https_t request) {
|
||||||
print_req(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
|
// Don't do anything if file does not exist or is outside the assets directory
|
||||||
if (!isChildPath(filePath, nodeModulesPath)) {
|
if (!isChildPath(filePath, nodeModulesPath)) {
|
||||||
BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the assets folder";
|
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)) {
|
if (!fs::exists(filePath)) {
|
||||||
response->write(SimpleWeb::StatusCode::client_error_not_found);
|
not_found(response, request);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
auto relPath = fs::relative(filePath, webDirPath);
|
auto relPath = fs::relative(filePath, webDirPath);
|
||||||
// get the mime type from the file extension mime_types map
|
// get the mime type from the file extension mime_types map
|
||||||
// remove the leading period from the extension
|
// remove the leading period from the extension
|
||||||
auto mimeType = mime_types.find(relPath.extension().string().substr(1));
|
auto mimeType = mime_types.find(relPath.extension().string().substr(1));
|
||||||
// check if the extension is in the map at the x position
|
// check if the extension is in the map at the x position
|
||||||
if (mimeType != mime_types.end()) {
|
if (mimeType == mime_types.end()) {
|
||||||
// if it is, set the content type to the mime type
|
bad_request(response, request);
|
||||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
return;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
* @brief Get the list of available applications.
|
||||||
* @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| GET| null}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
getApps(resp_https_t response, req_https_t request) {
|
getApps(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -346,6 +471,8 @@ namespace confighttp {
|
||||||
* @brief Get the logs from the log file.
|
* @brief Get the logs from the log file.
|
||||||
* @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/logs| GET| null}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
getLogs(resp_https_t response, req_https_t request) {
|
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 response The HTTP response object.
|
||||||
* @param request The HTTP request object.
|
* @param request The HTTP request object.
|
||||||
* The body for the post request should be JSON serialized in the following format:
|
* The body for the post request should be JSON serialized in the following format:
|
||||||
|
|
@ -385,9 +512,11 @@ namespace confighttp {
|
||||||
* "detached": [
|
* "detached": [
|
||||||
* "Detached command"
|
* "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
|
* @endcode
|
||||||
|
*
|
||||||
|
* @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
saveApp(resp_https_t response, req_https_t request) {
|
saveApp(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -398,19 +527,12 @@ namespace confighttp {
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << request->content.rdbuf();
|
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;
|
BOOST_LOG(info) << config::stream.file_apps;
|
||||||
try {
|
try {
|
||||||
// TODO: Input Validation
|
// TODO: Input Validation
|
||||||
|
pt::ptree fileTree;
|
||||||
|
pt::ptree inputTree;
|
||||||
|
pt::ptree outputTree;
|
||||||
pt::read_json(ss, inputTree);
|
pt::read_json(ss, inputTree);
|
||||||
pt::read_json(config::stream.file_apps, fileTree);
|
pt::read_json(config::stream.file_apps, fileTree);
|
||||||
|
|
||||||
|
|
@ -464,23 +586,23 @@ namespace confighttp {
|
||||||
fileTree.add_child("apps", sorted_apps);
|
fileTree.add_child("apps", sorted_apps);
|
||||||
|
|
||||||
pt::write_json(config::stream.file_apps, fileTree);
|
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) {
|
catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
||||||
|
bad_request(response, request, e.what());
|
||||||
outputTree.put("status", "false");
|
|
||||||
outputTree.put("error", "Invalid Input JSON");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outputTree.put("status", "true");
|
|
||||||
proc::refresh(config::stream.file_apps);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Delete an application.
|
* @brief Delete an application.
|
||||||
* @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}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
deleteApp(resp_https_t response, req_https_t request) {
|
deleteApp(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -489,46 +611,46 @@ namespace confighttp {
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
pt::ptree outputTree;
|
pt::ptree outputTree;
|
||||||
auto g = util::fail_guard([&]() {
|
|
||||||
std::ostringstream data;
|
|
||||||
|
|
||||||
pt::write_json(data, outputTree);
|
|
||||||
response->write(data.str());
|
|
||||||
});
|
|
||||||
pt::ptree fileTree;
|
|
||||||
try {
|
try {
|
||||||
|
pt::ptree fileTree;
|
||||||
|
pt::ptree newApps;
|
||||||
pt::read_json(config::stream.file_apps, fileTree);
|
pt::read_json(config::stream.file_apps, fileTree);
|
||||||
auto &apps_node = fileTree.get_child("apps"s);
|
auto &apps_node = fileTree.get_child("apps"s);
|
||||||
int index = stoi(request->path_match[1]);
|
int index = stoi(request->path_match[1]);
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0 || index >= static_cast<int>(apps_node.size())) {
|
||||||
outputTree.put("status", "false");
|
std::string error;
|
||||||
outputTree.put("error", "Invalid Index");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
|
// Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
|
||||||
pt::ptree newApps;
|
int i = 0;
|
||||||
int i = 0;
|
for (const auto &[k, v] : apps_node) {
|
||||||
for (const auto &[k, v] : apps_node) {
|
if (i++ != index) {
|
||||||
if (i++ != index) {
|
newApps.push_back(std::make_pair("", v));
|
||||||
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);
|
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) {
|
catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
|
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
|
||||||
outputTree.put("status", "false");
|
bad_request(response, request, e.what());
|
||||||
outputTree.put("error", "Invalid File JSON");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outputTree.put("status", "true");
|
|
||||||
proc::refresh(config::stream.file_apps);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -539,9 +661,11 @@ namespace confighttp {
|
||||||
* @code{.json}
|
* @code{.json}
|
||||||
* {
|
* {
|
||||||
* "key": "igdb_<game_id>",
|
* "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
|
* @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
|
void
|
||||||
uploadCover(resp_https_t response, req_https_t request) {
|
uploadCover(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -551,31 +675,19 @@ namespace confighttp {
|
||||||
std::stringstream configStream;
|
std::stringstream configStream;
|
||||||
ss << request->content.rdbuf();
|
ss << request->content.rdbuf();
|
||||||
pt::ptree outputTree;
|
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;
|
pt::ptree inputTree;
|
||||||
try {
|
try {
|
||||||
pt::read_json(ss, inputTree);
|
pt::read_json(ss, inputTree);
|
||||||
}
|
}
|
||||||
catch (std::exception &e) {
|
catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "UploadCover: "sv << e.what();
|
BOOST_LOG(warning) << "UploadCover: "sv << e.what();
|
||||||
outputTree.put("status", "false");
|
bad_request(response, request, e.what());
|
||||||
outputTree.put("error", e.what());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto key = inputTree.get("key", "");
|
auto key = inputTree.get("key", "");
|
||||||
if (key.empty()) {
|
if (key.empty()) {
|
||||||
outputTree.put("error", "Cover key is required");
|
bad_request(response, request, "Cover key is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto url = inputTree.get("url", "");
|
auto url = inputTree.get("url", "");
|
||||||
|
|
@ -586,11 +698,11 @@ namespace confighttp {
|
||||||
std::basic_string path = coverdir + http::url_escape(key) + ".png";
|
std::basic_string path = coverdir + http::url_escape(key) + ".png";
|
||||||
if (!url.empty()) {
|
if (!url.empty()) {
|
||||||
if (http::url_get_host(url) != "images.igdb.com") {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!http::download_file(url, path)) {
|
if (!http::download_file(url, path)) {
|
||||||
outputTree.put("error", "Failed to download cover");
|
bad_request(response, request, "Failed to download cover");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -600,13 +712,17 @@ namespace confighttp {
|
||||||
std::ofstream imgfile(path);
|
std::ofstream imgfile(path);
|
||||||
imgfile.write(data.data(), (int) data.size());
|
imgfile.write(data.data(), (int) data.size());
|
||||||
}
|
}
|
||||||
|
outputTree.put("status", true);
|
||||||
outputTree.put("path", path);
|
outputTree.put("path", path);
|
||||||
|
send_response(response, outputTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the configuration settings.
|
* @brief Get the configuration settings.
|
||||||
* @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/config| GET| null}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
getConfig(resp_https_t response, req_https_t request) {
|
getConfig(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -615,14 +731,7 @@ namespace confighttp {
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
pt::ptree outputTree;
|
pt::ptree outputTree;
|
||||||
auto g = util::fail_guard([&]() {
|
outputTree.put("status", true);
|
||||||
std::ostringstream data;
|
|
||||||
|
|
||||||
pt::write_json(data, outputTree);
|
|
||||||
response->write(data.str());
|
|
||||||
});
|
|
||||||
|
|
||||||
outputTree.put("status", "true");
|
|
||||||
outputTree.put("platform", SUNSHINE_PLATFORM);
|
outputTree.put("platform", SUNSHINE_PLATFORM);
|
||||||
outputTree.put("version", PROJECT_VER);
|
outputTree.put("version", PROJECT_VER);
|
||||||
|
|
||||||
|
|
@ -631,12 +740,16 @@ namespace confighttp {
|
||||||
for (auto &[name, value] : vars) {
|
for (auto &[name, value] : vars) {
|
||||||
outputTree.put(std::move(name), std::move(value));
|
outputTree.put(std::move(name), std::move(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
send_response(response, outputTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the locale setting. This endpoint does not require authentication.
|
* @brief Get the locale setting. This endpoint does not require authentication.
|
||||||
* @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/configLocale| GET| null}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
getLocale(resp_https_t response, req_https_t request) {
|
getLocale(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -645,15 +758,9 @@ namespace confighttp {
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
pt::ptree outputTree;
|
pt::ptree outputTree;
|
||||||
auto g = util::fail_guard([&]() {
|
outputTree.put("status", true);
|
||||||
std::ostringstream data;
|
|
||||||
|
|
||||||
pt::write_json(data, outputTree);
|
|
||||||
response->write(data.str());
|
|
||||||
});
|
|
||||||
|
|
||||||
outputTree.put("status", "true");
|
|
||||||
outputTree.put("locale", config::sunshine.locale);
|
outputTree.put("locale", config::sunshine.locale);
|
||||||
|
send_response(response, outputTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -668,6 +775,8 @@ namespace confighttp {
|
||||||
* @endcode
|
* @endcode
|
||||||
*
|
*
|
||||||
* @attention{It is recommended to ONLY save the config settings that differ from the default behavior.}
|
* @attention{It is recommended to ONLY save the config settings that differ from the default behavior.}
|
||||||
|
*
|
||||||
|
* @api_examples{/api/config| POST| {"key":"value"}}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
saveConfig(resp_https_t response, req_https_t request) {
|
saveConfig(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -678,16 +787,10 @@ namespace confighttp {
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
std::stringstream configStream;
|
std::stringstream configStream;
|
||||||
ss << request->content.rdbuf();
|
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 {
|
try {
|
||||||
// TODO: Input Validation
|
// TODO: Input Validation
|
||||||
|
pt::ptree inputTree;
|
||||||
|
pt::ptree outputTree;
|
||||||
pt::read_json(ss, inputTree);
|
pt::read_json(ss, inputTree);
|
||||||
for (const auto &[k, v] : inputTree) {
|
for (const auto &[k, v] : inputTree) {
|
||||||
std::string value = inputTree.get<std::string>(k);
|
std::string value = inputTree.get<std::string>(k);
|
||||||
|
|
@ -696,12 +799,12 @@ namespace confighttp {
|
||||||
configStream << k << " = " << value << std::endl;
|
configStream << k << " = " << value << std::endl;
|
||||||
}
|
}
|
||||||
file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str());
|
file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str());
|
||||||
|
outputTree.put("status", true);
|
||||||
|
send_response(response, outputTree);
|
||||||
}
|
}
|
||||||
catch (std::exception &e) {
|
catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
|
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
|
||||||
outputTree.put("status", "false");
|
bad_request(response, request, e.what());
|
||||||
outputTree.put("error", e.what());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -709,6 +812,8 @@ namespace confighttp {
|
||||||
* @brief Restart Sunshine.
|
* @brief Restart Sunshine.
|
||||||
* @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/restart| POST| null}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
restart(resp_https_t response, req_https_t request) {
|
restart(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -720,6 +825,24 @@ namespace confighttp {
|
||||||
platf::restart();
|
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.
|
* @brief Update existing credentials.
|
||||||
* @param response The HTTP response object.
|
* @param response The HTTP response object.
|
||||||
|
|
@ -734,43 +857,24 @@ namespace confighttp {
|
||||||
* "confirmNewPassword": "Confirm New Password"
|
* "confirmNewPassword": "Confirm New Password"
|
||||||
* }
|
* }
|
||||||
* @endcode
|
* @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
|
void
|
||||||
savePassword(resp_https_t response, req_https_t request) {
|
savePassword(resp_https_t response, req_https_t request) {
|
||||||
if (!config::sunshine.username.empty() && !authenticate(response, request)) return;
|
if (!config::sunshine.username.empty() && !authenticate(response, request)) return;
|
||||||
|
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
|
std::vector<std::string> errors = {};
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
std::stringstream configStream;
|
std::stringstream configStream;
|
||||||
ss << request->content.rdbuf();
|
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 {
|
try {
|
||||||
// TODO: Input Validation
|
// TODO: Input Validation
|
||||||
|
pt::ptree inputTree;
|
||||||
|
pt::ptree outputTree;
|
||||||
pt::read_json(ss, inputTree);
|
pt::read_json(ss, inputTree);
|
||||||
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
|
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
|
||||||
auto newUsername = inputTree.get<std::string>("newUsername");
|
auto newUsername = inputTree.get<std::string>("newUsername");
|
||||||
|
|
@ -779,15 +883,13 @@ namespace confighttp {
|
||||||
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("confirmNewPassword") : "";
|
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("confirmNewPassword") : "";
|
||||||
if (newUsername.length() == 0) newUsername = username;
|
if (newUsername.length() == 0) newUsername = username;
|
||||||
if (newUsername.length() == 0) {
|
if (newUsername.length() == 0) {
|
||||||
outputTree.put("status", false);
|
errors.emplace_back("Invalid Username");
|
||||||
outputTree.put("error", "Invalid Username");
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
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 (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) {
|
||||||
if (newPassword.empty() || newPassword != confirmPassword) {
|
if (newPassword.empty() || newPassword != confirmPassword) {
|
||||||
outputTree.put("status", false);
|
errors.emplace_back("Password Mismatch");
|
||||||
outputTree.put("error", "Password Mismatch");
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
||||||
|
|
@ -796,16 +898,25 @@ namespace confighttp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
outputTree.put("status", false);
|
errors.emplace_back("Invalid Current Credentials");
|
||||||
outputTree.put("error", "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) {
|
catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
|
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
|
||||||
outputTree.put("status", false);
|
bad_request(response, request, e.what());
|
||||||
outputTree.put("error", e.what());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -820,6 +931,8 @@ namespace confighttp {
|
||||||
* "name": "Friendly Client Name"
|
* "name": "Friendly Client Name"
|
||||||
* }
|
* }
|
||||||
* @endcode
|
* @endcode
|
||||||
|
*
|
||||||
|
* @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
savePin(resp_https_t response, req_https_t request) {
|
savePin(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -830,26 +943,19 @@ namespace confighttp {
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << request->content.rdbuf();
|
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 {
|
try {
|
||||||
// TODO: Input Validation
|
// TODO: Input Validation
|
||||||
|
pt::ptree inputTree;
|
||||||
|
pt::ptree outputTree;
|
||||||
pt::read_json(ss, inputTree);
|
pt::read_json(ss, inputTree);
|
||||||
std::string pin = inputTree.get<std::string>("pin");
|
std::string pin = inputTree.get<std::string>("pin");
|
||||||
std::string name = inputTree.get<std::string>("name");
|
std::string name = inputTree.get<std::string>("name");
|
||||||
outputTree.put("status", nvhttp::pin(pin, name));
|
outputTree.put("status", nvhttp::pin(pin, name));
|
||||||
|
send_response(response, outputTree);
|
||||||
}
|
}
|
||||||
catch (std::exception &e) {
|
catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "SavePin: "sv << e.what();
|
BOOST_LOG(warning) << "SavePin: "sv << e.what();
|
||||||
outputTree.put("status", false);
|
bad_request(response, request, e.what());
|
||||||
outputTree.put("error", e.what());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -857,6 +963,8 @@ namespace confighttp {
|
||||||
* @brief Unpair all clients.
|
* @brief Unpair all clients.
|
||||||
* @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/clients/unpair-all| POST| null}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
unpairAll(resp_https_t response, req_https_t request) {
|
unpairAll(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -864,16 +972,12 @@ namespace confighttp {
|
||||||
|
|
||||||
print_req(request);
|
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();
|
nvhttp::erase_all_clients();
|
||||||
proc::proc.terminate();
|
proc::proc.terminate();
|
||||||
|
|
||||||
|
pt::ptree outputTree;
|
||||||
outputTree.put("status", true);
|
outputTree.put("status", true);
|
||||||
|
send_response(response, outputTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -886,6 +990,8 @@ namespace confighttp {
|
||||||
* "uuid": "<uuid>"
|
* "uuid": "<uuid>"
|
||||||
* }
|
* }
|
||||||
* @endcode
|
* @endcode
|
||||||
|
*
|
||||||
|
* @api_examples{/api/unpair| POST| {"uuid":"1234"}}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
unpair(resp_https_t response, req_https_t request) {
|
unpair(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -896,25 +1002,18 @@ namespace confighttp {
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << request->content.rdbuf();
|
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 {
|
try {
|
||||||
// TODO: Input Validation
|
// TODO: Input Validation
|
||||||
|
pt::ptree inputTree;
|
||||||
|
pt::ptree outputTree;
|
||||||
pt::read_json(ss, inputTree);
|
pt::read_json(ss, inputTree);
|
||||||
std::string uuid = inputTree.get<std::string>("uuid");
|
std::string uuid = inputTree.get<std::string>("uuid");
|
||||||
outputTree.put("status", nvhttp::unpair_client(uuid));
|
outputTree.put("status", nvhttp::unpair_client(uuid));
|
||||||
|
send_response(response, outputTree);
|
||||||
}
|
}
|
||||||
catch (std::exception &e) {
|
catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "Unpair: "sv << e.what();
|
BOOST_LOG(warning) << "Unpair: "sv << e.what();
|
||||||
outputTree.put("status", false);
|
bad_request(response, request, e.what());
|
||||||
outputTree.put("error", e.what());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -922,6 +1021,8 @@ namespace confighttp {
|
||||||
* @brief Get the list of paired clients.
|
* @brief Get the list of paired clients.
|
||||||
* @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/clients/list| GET| null}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
listClients(resp_https_t response, req_https_t request) {
|
listClients(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -929,26 +1030,21 @@ namespace confighttp {
|
||||||
|
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
pt::ptree named_certs = nvhttp::get_all_clients();
|
const pt::ptree named_certs = nvhttp::get_all_clients();
|
||||||
|
|
||||||
pt::ptree outputTree;
|
pt::ptree outputTree;
|
||||||
|
|
||||||
outputTree.put("status", false);
|
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.add_child("named_certs", named_certs);
|
||||||
outputTree.put("status", true);
|
outputTree.put("status", true);
|
||||||
|
send_response(response, outputTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Close the currently running application.
|
* @brief Close the currently running application.
|
||||||
* @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/close| POST| null}
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
closeApp(resp_https_t response, req_https_t request) {
|
closeApp(resp_https_t response, req_https_t request) {
|
||||||
|
|
@ -956,16 +1052,11 @@ namespace confighttp {
|
||||||
|
|
||||||
print_req(request);
|
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();
|
proc::proc.terminate();
|
||||||
|
|
||||||
|
pt::ptree outputTree;
|
||||||
outputTree.put("status", true);
|
outputTree.put("status", true);
|
||||||
|
send_response(response, outputTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -976,6 +1067,18 @@ namespace confighttp {
|
||||||
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
|
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
|
||||||
|
|
||||||
https_server_t server { config::nvhttp.cert, config::nvhttp.pkey };
|
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.default_resource["GET"] = not_found;
|
||||||
server.resource["^/$"]["GET"] = getIndexPage;
|
server.resource["^/$"]["GET"] = getIndexPage;
|
||||||
server.resource["^/pin/?$"]["GET"] = getPinPage;
|
server.resource["^/pin/?$"]["GET"] = getPinPage;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue