Merge with master

This commit is contained in:
Loki 2021-08-03 20:35:57 +02:00
commit 793e329fa5
13 changed files with 301 additions and 65 deletions

View File

@ -87,7 +87,8 @@ sunshine needs access to uinput to create mouse and gamepad events:
- When Moonlight request you insert the correct pin on sunshine:
- Type in the URL bar of your browser: `https://xxx.xxx.xxx.xxx:47990` where `xxx.xxx.xxx.xxx` is the IP address of your computer
- Ignore any warning given by your browser about "insecure website"
- Type in the username and password shown the first time you run Sunshine
- You should compile the next page with a new username and a password, needed to login into the next step
- Press "Save" and log in using the credentials given above
- Go to "PIN" in the Header
- Type in your PIN and press Enter, you should get a Success Message
- Click on one of the Applications listed
@ -99,16 +100,6 @@ All shortcuts start with CTRL + ALT + SHIFT, just like Moonlight
- CTRL + ALT + SHIFT + N --> Hide/Unhide the cursor (This may be usefull for Remote Desktop Mode for Moonlight)
- CTRL + ALT + SHIFT + F1/F13 --> Switch to different monitor for Streaming
## Note:
- The Windows key is not passed through by Moonlight, therefore Sunshine maps Right-Alt key to the Windows key
- If you set Video Bitrate to 0.5Mb/s:
- Sunshine will use CRF or QP to controll the quality of the stream. (See example configuration file for more details)
- This is less CPU intensive and it has lower average bandwith requirements compared to manually setting bitrate to acceptable quality
- However, it has higher peak bitrates, forcing Sunshine to drop entire frames when streaming 1080P due to their size.
- When this happens, the video portion of the stream appears to be frozen.
- This is rare enough that using this for the desktop environment is tolerable (in my opinion), however for gaming not so much.
## Credits:
- [Simple-Web-Server](https://gitlab.com/eidheim/Simple-Web-Server)
- [Moonlight](https://github.com/moonlight-stream)

View File

@ -77,6 +77,20 @@
# 3840x1600,
# ]
# Sometimes it may be usefull to map keybindings.
# Wayland won't allow clients to capture the Win Key for example
#
# See https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
#
# Note:
# keybindings needs to have a multiple of two elements
# keybindings = [
# 0x10, 0xA0,
# 0x11, 0xA2,
# 0x12, 0xA4,
# 0x4A, 0x4B
# ]
# How long to wait in milliseconds for data from moonlight before shutting down the stream
# ping_timeout = 10000

View File

@ -111,6 +111,18 @@
are supported.
</div>
</div>
<!-- Mapping Key AltRight to Key Windows -->
<div class="mb-3">
<label for="mapkey" class="form-label">Map Right Alt key to Windows key</label>
<select id="mapkey" class="form-select" v-model="config.key_rightalt_to_key_win">
<option value="disabled">Disabled</option>
<option value="enabled">Enabled</option>
</select>
</div>
<div class="form-text">
It may be possible that you cannot send the Windows Key from Moonlight directly.<br />
In those cases it may be usefull to make Sunshine think the Right Alt key is the Windows key
</div>
</div>
<!--Files Tab-->
<div v-if="currentTab === 'files'" class="config-page">
@ -533,6 +545,7 @@
delete this.config.status;
delete this.config.platform;
//Populate default values if not present in config
this.config.key_rightalt_to_key_win = this.config.key_rightalt_to_key_win || "disabled";
this.config.gamepad = this.config.gamepad || 'x360';
this.config.upnp = this.config.upnp || 'disabled';
this.config.min_log_level = this.config.min_log_level || 2;
@ -561,6 +574,7 @@
let nl = this.config === 'windows' ? "\r\n" : "\n";
this.config.resolutions = "[" + nl + " " + this.resolutions.join("," + nl + " ") + nl + "]";
this.config.fps = JSON.stringify(this.fps);
fetch("/api/config", {
method: "POST",
body: JSON.stringify(this.config)
@ -588,4 +602,4 @@
font-size: 12px;
font-weight: bold;
}
</style>
</style>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sunshine</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
</head>
<body>

69
assets/web/welcome.html Normal file
View File

@ -0,0 +1,69 @@
<main role="main" id="app" style="max-width: 600px;margin: 0 auto;">
<div class="container-parent">
<div class="container py-3">
<h1 class="mb-0">Welcome to Sunshine!</h1>
<p class="mb-0 align-self-start">Before Getting Started, write down below these credentials</p>
</div>
</div>
<div class="alert alert-warning">These Credentials down below are needed to access the rest of the application.<br> Keep them safe, since <b>you will never see them again!</b></div>
<form @submit.prevent="save" class="card p-4" style="width: 100%;">
<div class="mb-2">
<label for="" class="form-label">Username: </label>
<input type="text" class="form-control" v-model="passwordData.newUsername">
</div>
<div class="mb-2">
<label for="" class="form-label">Password: </label>
<input type="password" class="form-control" v-model="passwordData.newPassword">
</div>
<div class="mb-2">
<label for="" class="form-label">Password: </label>
<input type="password" class="form-control" v-model="passwordData.confirmNewPassword">
</div>
<button class="mb-2 btn btn-primary" style="margin: 1em auto;">Login</button>
<div class="alert alert-danger" v-if="error"><b>Error: </b>{{error}}</div>
<div class="alert alert-success" v-if="success"><b>Success! </b>This page will reload soon, your browser will ask you for the new credentials</div>
</form>
</div>
</main>
<script>
new Vue({
el: '#app',
data() {
return {
error: null,
success: false,
passwordData: {
newUsername: 'sunshine',
newPassword: '',
confirmNewPassword: ''
}
}
},
methods: {
save() {
this.error = null;
fetch("/api/password", {
method: "POST",
body: JSON.stringify(this.passwordData)
}).then((r) => {
if (r.status == 200){
r.json().then((rj) => {
if(rj.status.toString() === "true"){
this.success = true;
setTimeout(()=>{
document.location.reload();
},5000);
} else {
this.error = rj.error;
}
})
}
else {
this.error = "Internal Server Error"
}
});
}
}
})
</script>

View File

@ -210,6 +210,11 @@ nvhttp_t nvhttp {
};
input_t input {
{
{ 0x10, 0xA0 },
{ 0x11, 0xA2 },
{ 0x12, 0xA4 },
},
2s, // back_button_timeout
500ms, // key_repeat_delay
std::chrono::duration<double> { 1 / 24.9 }, // key_repeat_period
@ -399,8 +404,20 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
return;
}
auto &val = it->second;
input = util::from_chars(&val[0], &val[0] + val.size());
std::string_view val = it->second;
// If value is something like: "756" instead of 756
if(val.size() >= 2 && val[0] == '"') {
val = val.substr(1, val.size() - 2);
}
// If that integer is in hexadecimal
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
input = util::from_hex<int>(val.substr(2));
}
else {
input = util::from_view(val);
}
vars.erase(it);
}
@ -412,8 +429,20 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
return;
}
auto &val = it->second;
input = util::from_chars(&val[0], &val[0] + val.size());
std::string_view val = it->second;
// If value is something like: "756" instead of 756
if(val.size() >= 2 && val[0] == '"') {
val = val.substr(1, val.size() - 2);
}
// If that integer is in hexadecimal
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
input = util::from_hex<int>(val.substr(2));
}
else {
input = util::from_view(val);
}
vars.erase(it);
}
@ -545,7 +574,42 @@ void list_int_f(std::unordered_map<std::string, std::string> &vars, const std::s
list_string_f(vars, name, list);
for(auto &el : list) {
input.emplace_back(util::from_view(el));
std::string_view val = el;
// If value is something like: "756" instead of 756
if(val.size() >= 2 && val[0] == '"') {
val = val.substr(1, val.size() - 2);
}
int tmp;
// If the integer is a hexadecimal
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
tmp = util::from_hex<int>(val.substr(2));
}
else {
tmp = util::from_view(val);
}
input.emplace_back(tmp);
}
}
void map_int_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::unordered_map<int, int> &input) {
std::vector<int> list;
list_int_f(vars, name, list);
// The list needs to be a multiple of 2
if(list.size() % 2) {
std::cout << "Warning: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size() << std::endl;
return;
}
int x = 0;
while(x < list.size()) {
auto key = list[x++];
auto val = list[x++];
input.emplace(key, val);
}
}
@ -633,6 +697,17 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
path_f(vars, "file_apps", stream.file_apps);
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 });
map_int_int_f(vars, "keybindings"s, input.keybindings);
// This config option will only be used by the UI
// When editing in the config file itself, use "keybindings"
bool map_rightalt_to_win = false;
bool_f(vars, "key_rightalt_to_key_win", map_rightalt_to_win);
if(map_rightalt_to_win) {
input.keybindings.emplace(0xA5, 0x5B);
}
to = std::numeric_limits<int>::min();
int_f(vars, "back_button_timeout", to);

View File

@ -11,7 +11,7 @@
namespace config {
struct video_t {
// ffmpeg params
int qp; // higher == more compression and less quality
int qp; // higher == more compression and less quality
int hevc_mode;
@ -73,6 +73,8 @@ struct nvhttp_t {
};
struct input_t {
std::unordered_map<int, int> keybindings;
std::chrono::milliseconds back_button_timeout;
std::chrono::milliseconds key_repeat_delay;
std::chrono::duration<double> key_repeat_period;

View File

@ -73,6 +73,15 @@ void send_unauthorized(resp_https_t response, req_https_t request) {
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
}
void send_redirect(resp_https_t response, req_https_t request, const char *path) {
auto address = request->remote_endpoint_address();
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
const SimpleWeb::CaseInsensitiveMultimap headers {
{ "Location", path }
};
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);
}
bool authenticate(resp_https_t response, req_https_t request) {
auto address = request->remote_endpoint_address();
auto ip_type = net::from_address(address);
@ -83,6 +92,12 @@ bool authenticate(resp_https_t response, req_https_t request) {
return false;
}
//If credentials are shown, redirect the user to a /welcome page
if(config::sunshine.username.empty()){
send_redirect(response,request,"/welcome");
return false;
}
auto fg = util::fail_guard([&]() {
send_unauthorized(response, request);
});
@ -185,6 +200,17 @@ void getPasswordPage(resp_https_t response, req_https_t request) {
response->write(header + content);
}
void getWelcomePage(resp_https_t response, req_https_t request) {
print_req(request);
if(!config::sunshine.username.empty()){
send_redirect(response,request,"/");
return;
}
std::string header = read_file(WEB_DIR "header-no-nav.html");
std::string content = read_file(WEB_DIR "welcome.html");
response->write(header + content);
}
void getApps(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
@ -371,7 +397,7 @@ void saveConfig(resp_https_t response, req_https_t request) {
}
void savePassword(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
if(!config::sunshine.username.empty() && !authenticate(response, request)) return;
print_req(request);
@ -390,27 +416,31 @@ void savePassword(resp_https_t response, req_https_t request) {
try {
//TODO: Input Validation
pt::read_json(ss, inputTree);
auto username = inputTree.get<std::string>("currentUsername");
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
auto newUsername = inputTree.get<std::string>("newUsername");
auto password = inputTree.get<std::string>("currentPassword");
auto password = inputTree.count("currentPassword") > 0 ? inputTree.get<std::string>("currentPassword") : "";
auto newPassword = inputTree.get<std::string>("newPassword");
auto confirmPassword = inputTree.get<std::string>("confirmNewPassword");
if(newUsername.length() == 0) newUsername = username;
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
if(username == config::sunshine.username && hash == config::sunshine.password) {
if(newPassword != confirmPassword) {
outputTree.put("status", false);
outputTree.put("error", "Password Mismatch");
}
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
http::reload_user_creds(config::sunshine.credentials_file);
outputTree.put("status", true);
}
else {
if(newUsername.length() == 0){
outputTree.put("status", false);
outputTree.put("error", "Invalid Current Credentials");
outputTree.put("error", "Invalid Username");
} else {
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
if(config::sunshine.username.empty() || (username == config::sunshine.username && hash == config::sunshine.password)) {
if(newPassword != confirmPassword) {
outputTree.put("status", false);
outputTree.put("error", "Password Mismatch");
} else {
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
http::reload_user_creds(config::sunshine.credentials_file);
outputTree.put("status", true);
}
}
else {
outputTree.put("status", false);
outputTree.put("error", "Invalid Current Credentials");
}
}
}
catch(std::exception &e) {
@ -467,6 +497,7 @@ void start() {
server.resource["^/clients$"]["GET"] = getClientsPage;
server.resource["^/config$"]["GET"] = getConfigPage;
server.resource["^/password$"]["GET"] = getPasswordPage;
server.resource["^/welcome$"]["GET"] = getWelcomePage;
server.resource["^/api/pin"]["POST"] = savePin;
server.resource["^/api/apps$"]["GET"] = getApps;
server.resource["^/api/apps$"]["POST"] = saveApp;

View File

@ -54,15 +54,11 @@ int init() {
return -1;
}
}
if(!user_creds_exist(config::sunshine.credentials_file)) {
if(save_user_creds(config::sunshine.credentials_file, "sunshine"s, crypto::rand_alphabet(16), true)) {
return -1;
}
if(user_creds_exist(config::sunshine.credentials_file)) {
if(reload_user_creds(config::sunshine.credentials_file)) return -1;
} else {
BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started";
}
if(reload_user_creds(config::sunshine.credentials_file)) {
return -1;
}
return 0;
}
@ -92,12 +88,6 @@ int save_user_creds(const std::string &file, const std::string &username, const
}
BOOST_LOG(info) << "New credentials have been created"sv;
if(run_our_mouth) {
BOOST_LOG(info) << "Username: "sv << username;
BOOST_LOG(info) << "Password: "sv << password;
}
return 0;
}

View File

@ -409,13 +409,9 @@ void repeat_key(short key_code) {
}
short map_keycode(short keycode) {
switch(keycode) {
case 0x10:
return 0xA0;
case 0x11:
return 0xA2;
case 0x12:
return 0xA4;
auto it = config::input.keybindings.find(keycode);
if(it != std::end(config::input.keybindings)) {
return it->second;
}
return keycode;

View File

@ -190,7 +190,7 @@ stream::launch_session_t make_launch_session(bool host_audio, const args_t &args
stream::launch_session_t launch_session;
launch_session.host_audio = host_audio;
launch_session.gcm_key = *util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
launch_session.gcm_key = util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
auto prepend_iv_p = (uint8_t *)&prepend_iv;
@ -211,7 +211,7 @@ void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
auto key = crypto::gen_aes_key(*salt, pin);
auto key = crypto::gen_aes_key(salt, pin);
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
tree.put("root.paired", 1);

View File

@ -720,6 +720,42 @@ std::shared_ptr<display_t> display(platf::mem_type_e hwdevice_type, int framerat
return x11_disp;
}
std::vector<std::string> display_names() {
if(xcb::init_shm() || xcb::init() || x11::init() || x11::rr::init() || x11::fix::init()) {
BOOST_LOG(error) << "Couldn't init x11 libraries"sv;
return nullptr;
}
BOOST_LOG(info) << "Detecting connected monitors"sv;
xdisplay_t xdisplay { x11::OpenDisplay(nullptr) };
if(!xdisplay) {
return {};
}
auto xwindow = DefaultRootWindow(xdisplay.get());
screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };
int output = screenr->noutput;
int monitor = 0;
for(int x = 0; x < output; ++x) {
output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
if(out_info && out_info->connection == RR_Connected) {
++monitor;
}
}
std::vector<std::string> names;
names.reserve(monitor);
for(auto x = 0; x < monitor; ++x) {
names.emplace_back(std::to_string(x));
}
return names;
}
void freeImage(XImage *p) {
XDestroyImage(p);
}

View File

@ -268,11 +268,12 @@ std::string hex_vec(C &&c, bool rev = false) {
}
template<class T>
std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
T from_hex(const std::string_view &hex, bool rev = false) {
std::uint8_t buf[sizeof(T)];
static char constexpr shift_bit = 'a' - 'A';
auto is_convertable = [](char ch) -> bool {
auto is_convertable = [](char ch) -> bool {
if(isdigit(ch)) {
return true;
}
@ -287,9 +288,7 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
};
auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;
if(buf_size != sizeof(T)) {
return std::nullopt;
}
auto padding = sizeof(T) - buf_size;
const char *data = hex.data() + hex.size() - 1;
@ -301,7 +300,9 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
return (std::uint8_t)(ch | (char)32) - 'a' + (char)10;
};
for(auto &el : buf) {
std::fill_n(buf + buf_size, padding, 0);
std::for_each_n(buf, buf_size, [&](auto &el) {
while(!is_convertable(*data)) { --data; }
std::uint8_t ch_r = convert(*data--);
@ -309,7 +310,7 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
std::uint8_t ch_l = convert(*data--);
el = (ch_l << 4) | ch_r;
}
});
if(rev) {
std::reverse(std::begin(buf), std::end(buf));