mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-01-29 09:32:39 +00:00
Merge branch 'master' into vaapi
This commit is contained in:
commit
877c739f1b
@ -35,6 +35,7 @@ if(WIN32)
|
||||
set(SUNSHINE_PREPARED_BINARIES "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled/windows")
|
||||
endif()
|
||||
|
||||
add_compile_definitions(SUNSHINE_PLATFORM="windows")
|
||||
add_subdirectory(tools) #This is temporary, only tools for Windows are needed, for now
|
||||
|
||||
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_windows.json")
|
||||
@ -91,6 +92,7 @@ if(WIN32)
|
||||
set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650")
|
||||
set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess")
|
||||
else()
|
||||
add_compile_definitions(SUNSHINE_PLATFORM="linux")
|
||||
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json")
|
||||
|
||||
find_package(X11 REQUIRED)
|
||||
@ -154,6 +156,10 @@ set(SUNSHINE_TARGET_FILES
|
||||
sunshine/crypto.h
|
||||
sunshine/nvhttp.cpp
|
||||
sunshine/nvhttp.h
|
||||
sunshine/httpcommon.cpp
|
||||
sunshine/httpcommon.h
|
||||
sunshine/confighttp.cpp
|
||||
sunshine/confighttp.h
|
||||
sunshine/rtsp.cpp
|
||||
sunshine/rtsp.h
|
||||
sunshine/stream.cpp
|
||||
|
@ -57,7 +57,7 @@ sunshine needs access to uinput to create mouse and gamepad events:
|
||||
- `groups $USER`
|
||||
|
||||
- If Sunshine sends audio from the microphone instead of the speaker, try the following steps:
|
||||
1. pacmd list-sources | grep "name:"
|
||||
1. `$ pacmd list-sources | grep "name:"` or `$ pactl info | grep Source` if running pipewire.
|
||||
2. Copy the name to the configuration option "audio_sink"
|
||||
3. restart sunshine
|
||||
|
||||
|
160
assets/web/apps.html
Normal file
160
assets/web/apps.html
Normal file
@ -0,0 +1,160 @@
|
||||
<div id="app" class="container">
|
||||
<div class="my-4">
|
||||
<h1>Applications</h1>
|
||||
<div>Applications are refreshed only when Client is restarted</div>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(app,i) in apps" :key="i">
|
||||
<td>{{app.name}}</td>
|
||||
<td><button class="btn btn-primary" @click="editApp(i)">Edit</button>
|
||||
<button class="btn btn-danger" @click="showDeleteForm(i)">Delete</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="edit-form card mt-2" v-if="showEditForm">
|
||||
<div class="p-4">
|
||||
<!--name-->
|
||||
<div class="mb-3">
|
||||
<label for="appName" class="form-label">Application Name</label>
|
||||
<input type="text" class="form-control" id="appName" aria-describedby="appNameHelp" v-model="editForm.name">
|
||||
<div id="appNameHelp" class="form-text">Application Name, as shown on Moonlight</div>
|
||||
</div>
|
||||
<!--output-->
|
||||
<div class="mb-3">
|
||||
<label for="appOutput" class="form-label">Output</label>
|
||||
<input type="text" class="form-control monospace" id="appOutput" aria-describedby="appOutputHelp"
|
||||
v-model="editForm.output">
|
||||
<div id="appOutputHelp" class="form-text">The file where the output of the command is stored, if it is not
|
||||
specified, the output is ignored</div>
|
||||
</div>
|
||||
<!--prep-cmd-->
|
||||
<div class="mb-3 d-flex flex-column">
|
||||
<label for="appName" class="form-label">Command Preparations</label>
|
||||
<div class="form-text">A list of commands to be run before/after the application. <br> If any of the
|
||||
prep-commands fail, starting the application is aborted</div>
|
||||
<table v-if="editForm['prep-cmd'].length > 0">
|
||||
<thead>
|
||||
<th class="precmd-head">Do</th>
|
||||
<th class="precmd-head">Undo</th>
|
||||
<th style="width: 48px;"></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(c,i) in editForm['prep-cmd']">
|
||||
<td><input type="text" class="form-control monospace" v-model="c.do"></td>
|
||||
<td><input type="text" class="form-control monospace" v-model="c.undo"></td>
|
||||
<td><button class="btn btn-danger" @click="editForm['prep-cmd'].splice(i,1)">×</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="mt-2 btn btn-success" style="margin: 0 auto;" @click="addPrepCmd">+ Add</button>
|
||||
</div>
|
||||
<!--detatched-->
|
||||
<div class="mb-3">
|
||||
<label for="appName" class="form-label">Detached Commands</label>
|
||||
<div v-for="(c,i) in editForm.detached" class="d-flex justify-content-between my-2">
|
||||
<pre>{{c}}</pre>
|
||||
<button class="btn btn-danger mx-2" @click="editForm.detached.splice(i,1)">×</button>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<input type="text" class="form-control monospace" v-model="detachedCmd">
|
||||
<button class="btn btn-success mx-2" @click="editForm.detached.push(detachedCmd);detachedCmd = '';">+</button>
|
||||
</div>
|
||||
<div class="form-text">A list of commands to be run and forgotten about</div>
|
||||
</div>
|
||||
<!--command-->
|
||||
<div class="mb-3">
|
||||
<label for="appCmd" class="form-label">Command</label>
|
||||
<input type="text" class="form-control monospace" id="appCmd" aria-describedby="appCmdHelp" v-model="editForm.cmd">
|
||||
<div id="appCmdHelp" class="form-text">The main application, if it is not specified, a processs is started that
|
||||
sleeps indefinitely</div>
|
||||
</div>
|
||||
<!--buttons-->
|
||||
<div class="d-flex">
|
||||
<button @click="showEditForm = false" class="btn btn-secondary m-2">Cancel</button>
|
||||
<button class="btn btn-primary m-2" @click="save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2" v-else >
|
||||
<button class="btn btn-primary" @click="newApp">+ Add New</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
apps: [],
|
||||
showEditForm: false,
|
||||
editForm: null,
|
||||
detachedCmd: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
fetch("/api/apps").then(r => r.json()).then((r) => {
|
||||
console.log(r);
|
||||
this.apps = r.apps;
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
newApp() {
|
||||
this.editForm = {
|
||||
name: '',
|
||||
output: '',
|
||||
cmd: [],
|
||||
index: -1,
|
||||
"prep-cmd": []
|
||||
};
|
||||
this.editForm.index = -1;
|
||||
this.showEditForm = true;
|
||||
},
|
||||
editApp(id) {
|
||||
this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
|
||||
this.$set(this.editForm, "index", id);
|
||||
if (this.editForm["prep-cmd"] === undefined) this.$set(this.editForm, "prep-cmd", []);
|
||||
if (this.editForm["detached"] === undefined) this.$set(this.editForm, "detached", []);
|
||||
this.showEditForm = true;
|
||||
},
|
||||
showDeleteForm(id) {
|
||||
let resp = confirm("Are you sure to delete " + this.apps[id].name + "?");
|
||||
if (resp) {
|
||||
fetch("/api/apps/" + id, { method: "DELETE" }).then((r) => {
|
||||
if (r.status == 200) document.location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
addPrepCmd() {
|
||||
this.editForm['prep-cmd'].push({
|
||||
do: '',
|
||||
undo: '',
|
||||
});
|
||||
},
|
||||
save() {
|
||||
fetch("/api/apps", { method: "POST", body: JSON.stringify(this.editForm) }).then((r) => {
|
||||
if (r.status == 200) document.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.precmd-head {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
</style>
|
3
assets/web/clients.html
Normal file
3
assets/web/clients.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div id="content" class="container">
|
||||
<h1>Clients</h1>
|
||||
</div>
|
492
assets/web/config.html
Normal file
492
assets/web/config.html
Normal file
@ -0,0 +1,492 @@
|
||||
<div id="app" class="container">
|
||||
<h1 class="my-4">Configuration</h1>
|
||||
<div class="form" v-if="config">
|
||||
<!--Header-->
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item" v-for="tab in tabs" :key="tab.id">
|
||||
<a class="nav-link" :class="{'active': tab.id === currentTab}" href="#"
|
||||
@click="currentTab = tab.id">{{tab.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!--General Tab-->
|
||||
<div v-if="currentTab === 'general'" class="config-page">
|
||||
<!--Sunshine Name-->
|
||||
<div class="mb-3">
|
||||
<label for="sunshine_name" class="form-label">Sunshine Name</label>
|
||||
<input type="text" class="form-control" id="sunshine_name" placeholder="Sunshine" v-model="config.sunshine_name">
|
||||
<div class="form-text">The name displayed by Moonlight. If not specified, the PC's hostname is used
|
||||
</div>
|
||||
</div>
|
||||
<!--Log Level-->
|
||||
<div class="mb-3">
|
||||
<label for="min_log_level" class="form-label">Log Level</label>
|
||||
<select id="min_log_level" class="form-select" v-model="config.min_log_level">
|
||||
<option :value="0">Verbose</option>
|
||||
<option :value="1">Debug</option>
|
||||
<option :value="2">Info</option>
|
||||
<option :value="3">Warning</option>
|
||||
<option :value="4">Error</option>
|
||||
<option :value="5">Fatal</option>
|
||||
<option :value="6">None</option>
|
||||
</select>
|
||||
<div class="form-text">The minimum log level printed to standard out</div>
|
||||
</div>
|
||||
<!--Origin PIN Allowed-->
|
||||
<div class="mb-3">
|
||||
<label for="origin_pin_allowed" class="form-label">Origin PIN Allowed</label>
|
||||
<select id="origin_pin_allowed" class="form-select" v-model="config.origin_pin_allowed">
|
||||
<option value="pc">Only localhost may access /pin and Web UI</option>
|
||||
<option value="lan">Only those in LAN may access /pin and Web UI</option>
|
||||
<option value="wan">Anyone may access /pin and Web UI</option>
|
||||
</select>
|
||||
<div class="form-text">The origin of the remote endpoint address that is not denied for HTTP method /pin
|
||||
</div>
|
||||
</div>
|
||||
<!--External IP-->
|
||||
<div class="mb-3">
|
||||
<label for="external_ip" class="form-label">External IP</label>
|
||||
<input type="text" class="form-control" id="external_ip" placeholder="123.456.789.12" v-model="config.external_ip">
|
||||
<div class="form-text">If no external IP address is given, the local IP address is used</div>
|
||||
</div>
|
||||
<!--Ping Timeout-->
|
||||
<div class="mb-3">
|
||||
<label for="ping_timeout" class="form-label">Ping Timeout</label>
|
||||
<input type="text" class="form-control" id="ping_timeout" placeholder="2000" v-model="config.ping_timeout">
|
||||
<div class="form-text">How long to wait in milliseconds for data from moonlight before shutting down the
|
||||
stream</div>
|
||||
</div>
|
||||
<!--Advertised FPS and Resolutions-->
|
||||
<div class="mb-3">
|
||||
<label for="ping_timeout" class="form-label">Advertised Resolutions and FPS</label>
|
||||
<div class="resolutions-container">
|
||||
<label>Resolutions</label>
|
||||
<div class="resolutions d-flex flex-wrap">
|
||||
<div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(r,i) in resolutions" :key="r">
|
||||
<span class="px-2">{{r}}</span>
|
||||
<span style="cursor: pointer;" @click="resolutions.splice(i,1)">×</span>
|
||||
</div>
|
||||
<form @submit.prevent="resolutions.push(resIn);resIn = '';" class="d-flex align-items-center">
|
||||
<input type="text" v-model="resIn" required pattern="[0-9]+x[0-9]+" style="border-top-right-radius: 0;border-bottom-right-radius: 0;" class="form-control">
|
||||
<button style="border-top-left-radius: 0;border-bottom-left-radius: 0;" class="btn btn-success">+</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fps-container">
|
||||
<label>FPS</label>
|
||||
<div class="fps d-flex flex-wrap">
|
||||
<div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(f,i) in fps" :key="f">
|
||||
<span class="px-2">{{f}}</span>
|
||||
<span style="cursor: pointer;" @click="fps.splice(i,1)">×</span>
|
||||
</div>
|
||||
<form @submit.prevent="fps.push(fpsIn);fpsIn = '';" class="d-flex align-items-center">
|
||||
<input type="text" v-model="fpsIn" required pattern="[0-9]+"
|
||||
style="width: 6ch;border-top-right-radius: 0;border-bottom-right-radius: 0;" class="form-control">
|
||||
<button style="border-top-left-radius: 0;border-bottom-left-radius: 0;" class="btn btn-success">+</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
The display modes advertised by Sunshine<br>
|
||||
Some versions of Moonlight, such as Moonlight-nx (Switch),
|
||||
rely on this list to ensure that the requested resolutions and fps
|
||||
are supported.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Files Tab-->
|
||||
<div v-if="currentTab === 'files'" class="config-page">
|
||||
<!--Private Key-->
|
||||
<div class="mb-3">
|
||||
<label for="pkey" class="form-label">Private Key</label>
|
||||
<input type="text" class="form-control" id="pkey" placeholder="/dir/pkey.pem" v-model="config.pkey">
|
||||
<div class="form-text">The private key must be 2048 bits</div>
|
||||
</div>
|
||||
<!--Cert-->
|
||||
<div class="mb-3">
|
||||
<label for="cert" class="form-label">Cert</label>
|
||||
<input type="text" class="form-control" id="cert" placeholder="/dir/cert.pem" v-model="config.cert">
|
||||
<div class="form-text">The certificate must be signed with a 2048 bit key</div>
|
||||
</div>
|
||||
|
||||
<!--State File-->
|
||||
<div class="mb-3">
|
||||
<label for="file_state" class="form-label">State File</label>
|
||||
<input type="text" class="form-control" id="file_state" placeholder="sunshine_state.json" v-model="config.file_state">
|
||||
<div class="form-text">The file where current state of Sunshine is stored</div>
|
||||
</div>
|
||||
<!--Apps File-->
|
||||
<div class="mb-3">
|
||||
<label for="file_apps" class="form-label">Apps File</label>
|
||||
<input type="text" class="form-control" id="file_apps" placeholder="apps.json" v-model="config.file_apps">
|
||||
<div class="form-text">The file where current apps of Sunshine are stored</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentTab === 'input'" class="config-page">
|
||||
<!--Back Button Timeout-->
|
||||
<div class="mb-3">
|
||||
<label for="back_button_timeout" class="form-label">Back Button Timeout</label>
|
||||
<input type="text" class="form-control" id="back_button_timeout" placeholder="2000" v-model="config.back_button_timeout">
|
||||
<div class="form-text">
|
||||
The back/select button on the controller.<br>
|
||||
On the Shield, the home and powerbutton are not passed to Moonlight.<br>
|
||||
If, after the timeout, the back button is still pressed down, Home/Guide button press is
|
||||
emulated.<br>
|
||||
If back_button_timeout < 0, then the Home/Guide button will not be emulated<br>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Key Repeat Delay-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="key_repeat_delay" class="form-label">Key Repeat Delay</label>
|
||||
<input type="text" class="form-control" id="key_repeat_delay" placeholder="500" v-model="config.key_repeat_delay">
|
||||
<div class="form-text">
|
||||
Control how fast keys will repeat themselves<br>
|
||||
The initial delay in milliseconds before repeating keys
|
||||
</div>
|
||||
</div>
|
||||
<!-- Key Repeat Frequency-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="key_repeat_frequency" class="form-label">Key Repeat Frequency</label>
|
||||
<input type="text" class="form-control" id="key_repeat_frequency" placeholder="24.9" v-model="config.key_repeat_frequency">
|
||||
<div class="form-text">
|
||||
How often keys repeat every second<br>
|
||||
This configurable option supports decimals
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Files Tab-->
|
||||
<div v-if="currentTab === 'av'" class="config-page">
|
||||
<!--Audio Sink-->
|
||||
<div class="mb-3">
|
||||
<label for="audio_sink" class="form-label">Audio Sink</label>
|
||||
<input type="text" class="form-control" id="audio_sink" placeholder="" v-model="config.audio_sink">
|
||||
<div class="form-text" v-if="platform === 'windows'">
|
||||
The name of the audio sink used for Audio Loopback<br>
|
||||
You can find the name of the audio sink using the following command:<br>
|
||||
<pre>tools\audio-info.exe</pre>
|
||||
</div>
|
||||
<div class="form-text" v-if="platform === 'linux'">
|
||||
The name of the audio sink used for Audio Loopback<br>
|
||||
If you do not specify this variable, pulseaudio will select the default monitor device.<br>
|
||||
<br>
|
||||
You can find the name of the audio sink using the following command:<br>
|
||||
<pre>pacmd list-sinks | grep "name:"</pre><br>
|
||||
</div>
|
||||
</div>
|
||||
<!--Virtual Sink-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="virtual_sink" class="form-label">Virtual Sink</label>
|
||||
<input type="text" class="form-control" id="virtual_sink" placeholder="{0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}" v-model="config.virtual_sink">
|
||||
<div class="form-text">
|
||||
The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows Sunshine
|
||||
to stream audio, while muting the speakers.
|
||||
</div>
|
||||
</div>
|
||||
<!--Adapter Name -->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="adapter_name" class="form-label">Adapter Name</label>
|
||||
<input type="text" class="form-control" id="adapter_name" placeholder="Radeon RX 580 Series" v-model="config.adapter_name">
|
||||
<div class="form-text">
|
||||
You can select the video card you want to stream:<br>
|
||||
The appropriate values can be found using the following command:<br>
|
||||
<pre>tools\dxgi-info.exe</pre>
|
||||
</div>
|
||||
</div>
|
||||
<!--Output Name -->
|
||||
<div class="mb-3" class="config-page">
|
||||
<label for="output_name" class="form-label">Output Name</label>
|
||||
<input type="text" class="form-control" id="output_name" placeholder="\\.\DISPLAY1" v-model="config.output_name">
|
||||
<div class="form-text">
|
||||
You can select the video card you want to stream:<br>
|
||||
The appropriate values can be found using the following command:<br>
|
||||
tools\dxgi-info.exe<br>
|
||||
!! Linux only !!<br>
|
||||
Set the display number to stream. I have no idea how they are numbered. They start from 0, usually.<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentTab === 'advanced'" class="config-page">
|
||||
<!--Constant Rate Factor-->
|
||||
<div class="mb-3">
|
||||
<label for="crf" class="form-label">Constant Rate Factor</label>
|
||||
<input type="number" min="0" max="52" class="form-control" id="crf" placeholder="0" v-model="config.crf">
|
||||
<div class="form-text">
|
||||
Constant Rate Factor. Between 1 and 52. It allows QP to go up during motion and down with still
|
||||
image,
|
||||
resulting in constant perceived quality<br>
|
||||
Higher value means more compression, but less quality<br>
|
||||
If crf == 0, then use QP directly instead
|
||||
</div>
|
||||
</div>
|
||||
<!-- Quantization Parameter -->
|
||||
<div class="mb-3">
|
||||
<label for="qp" class="form-label">Quantitization Parameter</label>
|
||||
<input type="number" class="form-control" id="qp" placeholder="28" v-model="config.qp">
|
||||
<div class="form-text">
|
||||
Quantitization Parameter<br>
|
||||
Higher value means more compression, but less quality<br>
|
||||
If crf != 0, then this parameter is ignored
|
||||
</div>
|
||||
</div>
|
||||
<!-- Min Threads -->
|
||||
<div class="mb-3">
|
||||
<label for="min_threads" class="form-label">Minimum number of threads used by ffmpeg to encode the
|
||||
video.</label>
|
||||
<input type="number" min="1" class="form-control" id="min_threads" placeholder="1" v-model="config.min_threads">
|
||||
<div class="form-text">
|
||||
Minimum number of threads used by ffmpeg to encode the video.<br>
|
||||
Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually<br>
|
||||
worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest<br>
|
||||
value that can reliably encode at your desired streaming settings on your hardware.
|
||||
</div>
|
||||
</div>
|
||||
<!--HEVC Suppport -->
|
||||
<div class="mb-3">
|
||||
<label for="hevc_mode" class="form-label">HEVC Support</label>
|
||||
<select id="hevc_mode" class="form-select" v-model="config.hevc_mode">
|
||||
<option value="0">Sunshine will specify support for HEVC based on encoder</option>
|
||||
<option value="1">Sunshine will not advertise support for HEVC</option>
|
||||
<option value="2">Sunshine will advertise support for HEVC Main profile</option>
|
||||
<option value="3">Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Allows the client to request HEVC Main or HEVC Main10 video streams.<br>
|
||||
HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software
|
||||
encoding.
|
||||
</div>
|
||||
</div>
|
||||
<!--Encoder -->
|
||||
<div class="mb-3">
|
||||
<label for="encoder" class="form-label">Force a Specific Encoder</label>
|
||||
<select id="encoder" class="form-select" v-model="config.encoder">
|
||||
<option :value="''">Autodetect</option>
|
||||
<option value="nvenc">nVidia NVENC</option>
|
||||
<option value="amdvce">AMD AMF/VCE</option>
|
||||
<option value="software">Software</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Force a specific encoder, otherwise Sunshine will use the first encoder that is available
|
||||
</div>
|
||||
</div>
|
||||
<!--FEC Percentage-->
|
||||
<div class="mb-3">
|
||||
<label for="fec_percentage" class="form-label">FEC Percentage</label>
|
||||
<input type="text" class="form-control" id="fec_percentage" placeholder="10" v-model="config.fec_percentage">
|
||||
<div class="form-text">
|
||||
How much error correcting packets must be send for every video.<br>
|
||||
This is just some random number, don't know the optimal value.<br>
|
||||
The higher fec_percentage, the lower space for the actual data to send per frame there is
|
||||
</div>
|
||||
</div>
|
||||
<!--Channels-->
|
||||
<div class="mb-3">
|
||||
<label for="channels" class="form-label">Channels</label>
|
||||
<input type="text" class="form-control" id="channels" placeholder="1" v-model="config.channels">
|
||||
<div class="form-text">
|
||||
When multicasting, it could be useful to have different configurations for each connected Client.
|
||||
For example:
|
||||
<ul>
|
||||
<li>Clients connected through WAN and LAN have different bitrate contstraints.</li>
|
||||
<li>Decoders may require different settings for color</li>
|
||||
</ul>
|
||||
Unlike simply broadcasting to multiple Client, this will generate distinct video streams.<br>
|
||||
Note, CPU usage increases for each distinct video stream generated
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Software Settings-->
|
||||
<div v-if="currentTab === 'sw'" class="config-page">
|
||||
<div class="mb-3">
|
||||
<label for="sw_preset" class="form-label" >SW Presets</label>
|
||||
<input class="form-control" id="sw_preset" placeholder="superfast" v-model="config.sw_preset">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sw_tune" class="form-label">SW Tune</label>
|
||||
<input class="form-control" id="sw_tune" placeholder="zerolatency" v-model="config.sw_tune">
|
||||
</div>
|
||||
</div>
|
||||
<!--Nvidia Encoder Settings-->
|
||||
<div v-if="currentTab === 'nv'" class="config-page">
|
||||
<!--NVENC SETTINGS-->
|
||||
<div class="mb-3">
|
||||
<label for="nv_preset" class="form-label">NVEnc Preset</label>
|
||||
<select id="nv_preset" class="form-select" v-model="config.nv_preset">
|
||||
<option value="default">Default</option>
|
||||
<option value="hp">High Performance</option>
|
||||
<option value="hq">High Quality</option>
|
||||
<option value="slow">Slow - hq 2 passes</option>
|
||||
<option value="medium">medium -- hq 1 pass</option>
|
||||
<option value="fast">fast -- hp 1 pass</option>
|
||||
<option value="bd">bd</option>
|
||||
<option value="ll">ll -- low latency</option>
|
||||
<option value="llhq">llhq</option>
|
||||
<option value="llhp">llhp</option>
|
||||
<option value="lossless">lossless</option>
|
||||
<option value="losslesshp">losslesshp</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nv_rc" class="form-label">NVEnc Rate Control</label>
|
||||
<select id="nv_rc" class="form-select" v-model="config.nv_rc">
|
||||
<option value="auto">auto -- let ffmpeg decide rate control</option>
|
||||
<option value="constqp">constqp -- constant QP mode</option>
|
||||
<option value="vbr">vbr -- variable bitrate</option>
|
||||
<option value="cbr">cbr -- constant bitrate</option>
|
||||
<option value="cbr_hq">cbr_hq -- cbr high quality</option>
|
||||
<option value="cbr_ld_hq">cbr_ld_hq -- cbr low delay high quality</option>
|
||||
<option value="vbr_hq">vbr_hq -- vbr high quality</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nv_coder" class="form-label">NVEnc Coder</label>
|
||||
<select id="nv_coder" class="form-select" v-model="config.nv_coder">
|
||||
<option value="auto">auto</option>
|
||||
<option value="cabac">cabac</option>
|
||||
<option value="cavlc">cavlc</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!--AMD Encoder Settings-->
|
||||
<div v-if="currentTab === 'amd'" class="config-page">
|
||||
<!--Presets-->
|
||||
<div class="mb-3">
|
||||
<label for="amd_quality" class="form-label">AMD AMF Quality</label>
|
||||
<select id="amd_quality" class="form-select" v-model="config.amd_quality">
|
||||
<option value="default">Default</option>
|
||||
<option value="speed">Speed</option>
|
||||
<option value="balanced">Balanced</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amd_rc" class="form-label">AMD AMF Rate Control</label>
|
||||
<select id="amd_rc" class="form-select">
|
||||
<option value="auto">auto -- let ffmpeg decide rate control</option>
|
||||
<option value="constqp">constqp -- constant QP mode</option>
|
||||
<option value="vbr_latency">vbr_latency -- Latency Constrained Variable Bitrate</option>
|
||||
<option value="vbr_peak">vbr_peak -- Peak Contrained Variable Bitrate</option>
|
||||
<option value="cbr">cbr -- constant bitrate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amd_coder" class="form-label">AMD AMF Rate Control</label>
|
||||
<select id="amd_coder" class="form-select" v-model="config.amd_coder">
|
||||
<option value="auto">auto</option>
|
||||
<option value="cabac">cabac</option>
|
||||
<option value="cavlc">cavlc</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-success my-4" v-if="success"><b>Success!</b> Restart Sunshine to apply changes</div>
|
||||
<div class="mb-3 buttons">
|
||||
<button class="btn btn-primary" @click="save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
platform: '',
|
||||
success: false,
|
||||
config: null,
|
||||
fps: [],
|
||||
resolutions: [],
|
||||
currentTab: 'general',
|
||||
resIn: '',
|
||||
fpsIn: '',
|
||||
tabs: [{
|
||||
id: 'general',
|
||||
name: "General"
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
name: "Files"
|
||||
},
|
||||
{
|
||||
id: 'input',
|
||||
name: "Input"
|
||||
},
|
||||
{
|
||||
id: 'av',
|
||||
name: "Audio/Video"
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
name: "Advanced"
|
||||
},
|
||||
{
|
||||
id: "sw",
|
||||
name: "Software Encoder"
|
||||
},
|
||||
{
|
||||
id: "nv",
|
||||
name: "NVENC Encoder"
|
||||
},
|
||||
{
|
||||
id: "amd",
|
||||
name: "AMF Encoder"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
fetch("/api/config").then(r => r.json()).then((r) => {
|
||||
this.config = r;
|
||||
this.platform = this.config.platform;
|
||||
delete this.config.status;
|
||||
delete this.config.platform;
|
||||
//Populate default values if not present in config
|
||||
this.config.min_log_level = this.config.min_log_level || 2;
|
||||
this.config.origin_pin_allowed = this.config.origin_pin_allowed || "lan";
|
||||
this.config.hevc_mode = this.config.hevc_mode || 0;
|
||||
this.config.encoder = this.config.encoder || '';
|
||||
this.config.nv_preset = this.config.nv_preset || 'default';
|
||||
this.config.nv_rc = this.config.nv_rc || 'auto';
|
||||
this.config.nv_coder = this.config.nv_coder || 'auto';
|
||||
this.config.amd_quality = this.config.amd_quality || 'default';
|
||||
this.config.amd_rc = this.config.amd_rc || 'auto';
|
||||
this.config.fps = this.config.fps || '[10, 30, 60, 90, 120]';
|
||||
this.config.resolutions = this.config.resolutions || '[352x240,480x360,858x480,1280x720,1920x1080,2560x1080,3440x1440,1920x1200,3860x2160,3840x1600]';
|
||||
this.fps = JSON.parse(this.config.fps);
|
||||
//Resolutions should be fixed because are not valid JSON
|
||||
let res = this.config.resolutions.substring(1,this.config.resolutions.length-1);
|
||||
let resolutions = [];
|
||||
res.split(",").forEach(r => resolutions.push(r.trim()));
|
||||
this.resolutions = resolutions;
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.success = false;
|
||||
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)
|
||||
}).then((r) => {
|
||||
if (r.status == 200)this.success = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.config-page {
|
||||
padding: 1em;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.ms-item {
|
||||
background-color: #CCC;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
45
assets/web/header.html
Normal file
45
assets/web/header.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!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>
|
||||
<nav class="navbar navbar-expand-lg navbar-light" style="background-color: #ffc400;">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand">Sunshine</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/pin">PIN</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/apps">Applications</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/config">Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/password">Change Password</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
9
assets/web/index.html
Normal file
9
assets/web/index.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div id="content" class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 py-4" style="margin: 0 auto;">
|
||||
<h1>Hello, Sunshine!</h1>
|
||||
<p>Sunshine is a Gamestream host for Moonlight</p>
|
||||
<a href="https://github.com/loki-47-6F-64/sunshine">Official GitHub Repository</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
97
assets/web/password.html
Normal file
97
assets/web/password.html
Normal file
@ -0,0 +1,97 @@
|
||||
<div id="app" class="container">
|
||||
<h1 class="my-4">Password Change</h1>
|
||||
<form @submit.prevent="save">
|
||||
<div class="card d-flex p-4 flex-row">
|
||||
<div class="col-md-6 px-4">
|
||||
<h4>Current Credentials</h4>
|
||||
<div class="mb-3">
|
||||
<label for="currentUsername" class="form-label">Username</label>
|
||||
<input required type="text" class="form-control" id="currentUsername" v-model="passwordData.currentUsername">
|
||||
<div class="form-text"> </div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="currentPassword" class="form-label">Password</label>
|
||||
<input autocomplete="current-password" type="password" class="form-control" id="currentPassword" v-model="passwordData.currentPassword">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 px-4">
|
||||
<h4>New Credentials</h4>
|
||||
<div class="mb-3">
|
||||
<label for="newUsername" class="form-label">New Username</label>
|
||||
<input type="text" class="form-control" id="newUsername" v-model="passwordData.newUsername">
|
||||
<div class="form-text">If not specified, the username will not change
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newPassword" class="form-label">Password</label>
|
||||
<input autocomplete="new-password" required type="password" class="form-control" id="newPassword" v-model="passwordData.newPassword">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirmNewPassword" class="form-label">Confirm Password</label>
|
||||
<input autocomplete="new-password" required type="password" class="form-control" id="confirmNewPassword" v-model="passwordData.confirmNewPassword">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="mb-3 buttons">
|
||||
<button class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
success: false,
|
||||
passwordData: {
|
||||
currentUsername: '',
|
||||
currentPassword: '',
|
||||
newUsername: '',
|
||||
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>
|
||||
|
||||
<style>
|
||||
.config-page {
|
||||
padding: 1em;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: 1em 0;
|
||||
}
|
||||
</style>
|
31
assets/web/pin.html
Normal file
31
assets/web/pin.html
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
<div id="content" class="container">
|
||||
<h1 class="my-4">PIN Pairing</h1>
|
||||
<form action="" class="form d-flex flex-column align-items-center" id="form">
|
||||
<div class="card flex-column d-flex p-4 mb-4">
|
||||
<input type="number" placeholder="PIN" id="pin-input" class="form-control my-4">
|
||||
<button class="btn btn-primary">Send</button>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<b>Warning!</b> Make sure you have access to the client you are pairing with.<br>
|
||||
This software can give total control to your computer, so be careful!
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector("#form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
let pin = document.querySelector("#pin-input").value;
|
||||
document.querySelector("#status").innerHTML = "";
|
||||
let b = JSON.stringify({pin: pin});
|
||||
fetch("/api/pin",{method: "POST",body: b}).then((response) => response.json()).then((response)=>{
|
||||
if(response.status){
|
||||
document.querySelector("#status").innerHTML = `<div class="alert alert-success" role="alert">Success! Please check Moonlight to continue</div>`;
|
||||
} else {
|
||||
document.querySelector("#status").innerHTML = `<div class="alert alert-danger" role="alert">PIN does not match, please check if it's typed correctly</div>`;
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
@ -209,8 +209,12 @@ input_t input {
|
||||
};
|
||||
|
||||
sunshine_t sunshine {
|
||||
2, // min_log_level
|
||||
0 // flags
|
||||
2, // min_log_level
|
||||
0, // flags
|
||||
"user_credentials.json"s, //User file
|
||||
""s, //Username
|
||||
""s, //Password
|
||||
""s //Password Salt
|
||||
};
|
||||
|
||||
bool endline(char ch) {
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace config {
|
||||
@ -88,8 +89,11 @@ enum flag_e : std::size_t {
|
||||
|
||||
struct sunshine_t {
|
||||
int min_log_level;
|
||||
|
||||
std::bitset<flag::FLAG_SIZE> flags;
|
||||
std::string credentials_file;
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string salt;
|
||||
};
|
||||
|
||||
extern video_t video;
|
||||
@ -100,6 +104,6 @@ extern input_t input;
|
||||
extern sunshine_t sunshine;
|
||||
|
||||
int parse(int argc, char *argv[]);
|
||||
std::unordered_map<std::string, std::string> parse_config(std::string_view file_content);
|
||||
} // namespace config
|
||||
|
||||
#endif
|
||||
|
478
sunshine/confighttp.cpp
Normal file
478
sunshine/confighttp.cpp
Normal file
@ -0,0 +1,478 @@
|
||||
//
|
||||
// Created by TheElixZammuto on 2021-05-09.
|
||||
// TODO: Authentication, better handling of routes common to nvhttp, cleanup
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <Simple-Web-Server/crypto.hpp>
|
||||
#include <Simple-Web-Server/server_http.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
#include "crypto.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
std::string read_file(std::string path);
|
||||
|
||||
namespace confighttp {
|
||||
using namespace std::literals;
|
||||
constexpr auto PORT_HTTP = 47990;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
||||
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
||||
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
|
||||
|
||||
enum class op_e {
|
||||
ADD,
|
||||
REMOVE
|
||||
};
|
||||
|
||||
void send_unauthorized(resp_https_t response, req_https_t request) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" }
|
||||
};
|
||||
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
|
||||
}
|
||||
|
||||
bool authenticate(resp_https_t response, req_https_t request) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
if(ip_type > http::origin_pin_allowed) {
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
return false;
|
||||
}
|
||||
auto auth = request->header.find("authorization");
|
||||
if(auth == request->header.end()) {
|
||||
send_unauthorized(response, request);
|
||||
return false;
|
||||
}
|
||||
std::string rawAuth = auth->second;
|
||||
std::string authData = rawAuth.substr("Basic "sv.length());
|
||||
authData = SimpleWeb::Crypto::Base64::decode(authData);
|
||||
int index = authData.find(':');
|
||||
std::string username = authData.substr(0, index);
|
||||
std::string password = authData.substr(index + 1);
|
||||
std::string hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
if(username == config::sunshine.username && hash == config::sunshine.password) return true;
|
||||
|
||||
send_unauthorized(response, request);
|
||||
return false;
|
||||
}
|
||||
|
||||
void not_found(resp_https_t response, req_https_t request) {
|
||||
pt::ptree tree;
|
||||
tree.put("root.<xmlattr>.status_code", 404);
|
||||
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
|
||||
*response << "HTTP/1.1 404 NOT FOUND\r\n"
|
||||
<< data.str();
|
||||
}
|
||||
|
||||
void getIndexPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "index.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getPinPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "pin.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getAppsPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "apps.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getClientsPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "clients.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getConfigPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "config.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getPasswordPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "password.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getApps(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
std::string content = read_file(SUNSHINE_ASSETS_DIR "/" APPS_JSON);
|
||||
response->write(content);
|
||||
}
|
||||
|
||||
void saveApp(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
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;
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
pt::read_json(SUNSHINE_ASSETS_DIR "/" APPS_JSON, fileTree);
|
||||
auto &apps_node = fileTree.get_child("apps"s);
|
||||
int index = inputTree.get<int>("index");
|
||||
if(inputTree.get_child("prep-cmd").empty())
|
||||
inputTree.erase("prep-cmd");
|
||||
|
||||
if(inputTree.get_child("detached").empty())
|
||||
inputTree.erase("detached");
|
||||
|
||||
inputTree.erase("index");
|
||||
if(index == -1) {
|
||||
apps_node.push_back(std::make_pair("", inputTree));
|
||||
}
|
||||
else {
|
||||
//Unfortuantely Boost PT does not allow to directly edit the array, copy should do the trick
|
||||
pt::ptree newApps;
|
||||
int i = 0;
|
||||
for(const auto &kv : apps_node) {
|
||||
if(i == index) {
|
||||
newApps.push_back(std::make_pair("", inputTree));
|
||||
}
|
||||
else {
|
||||
newApps.push_back(std::make_pair("", kv.second));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
fileTree.erase("apps");
|
||||
fileTree.push_back(std::make_pair("apps", newApps));
|
||||
}
|
||||
pt::write_json(SUNSHINE_ASSETS_DIR "/" APPS_JSON, fileTree);
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(SUNSHINE_ASSETS_DIR "/" APPS_JSON);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Input JSON");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void deleteApp(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
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::read_json(config::stream.file_apps, fileTree);
|
||||
auto &apps_node = fileTree.get_child("apps"s);
|
||||
int index = stoi(request->path_match[1]);
|
||||
BOOST_LOG(info) << index;
|
||||
if(index <= 0) {
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Index");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
//Unfortuantely Boost PT does not allow to directly edit the array, copy should do the trick
|
||||
pt::ptree newApps;
|
||||
int i = 0;
|
||||
for(const auto &kv : apps_node) {
|
||||
if(i != index) {
|
||||
newApps.push_back(std::make_pair("", kv.second));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
fileTree.erase("apps");
|
||||
fileTree.push_back(std::make_pair("apps", newApps));
|
||||
}
|
||||
pt::write_json(SUNSHINE_ASSETS_DIR "/" APPS_JSON, fileTree);
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(SUNSHINE_ASSETS_DIR "/" APPS_JSON);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid File JSON");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void getConfig(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
try {
|
||||
outputTree.put("status", "true");
|
||||
outputTree.put("platform", SUNSHINE_PLATFORM);
|
||||
const char *config_file = SUNSHINE_ASSETS_DIR "/sunshine.conf";
|
||||
std::ifstream in { config_file };
|
||||
|
||||
if(!in.is_open()) {
|
||||
std::cout << "Error: Couldn't open "sv << config_file << std::endl;
|
||||
}
|
||||
|
||||
auto vars = config::parse_config(std::string {
|
||||
// Quick and dirty
|
||||
std::istreambuf_iterator<char>(in),
|
||||
std::istreambuf_iterator<char>() });
|
||||
|
||||
for(auto &[name, value] : vars) {
|
||||
outputTree.put(std::move(name), std::move(value));
|
||||
}
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid File JSON");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void saveConfig(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
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::read_json(ss, inputTree);
|
||||
for(const auto &kv : inputTree) {
|
||||
std::string value = inputTree.get<std::string>(kv.first);
|
||||
if(value.length() == 0 || value.compare("null") == 0) continue;
|
||||
|
||||
configStream << kv.first << " = " << value << std::endl;
|
||||
}
|
||||
http::write_file(SUNSHINE_ASSETS_DIR "/sunshine.conf", configStream.str());
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void savePassword(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree, fileTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
std::string username = inputTree.get<std::string>("currentUsername");
|
||||
std::string newUsername = inputTree.get<std::string>("newUsername");
|
||||
std::string password = inputTree.get<std::string>("currentPassword");
|
||||
std::string newPassword = inputTree.get<std::string>("newPassword");
|
||||
std::string confirmPassword = inputTree.get<std::string>("confirmNewPassword");
|
||||
if(newUsername.length() == 0) newUsername = username;
|
||||
|
||||
std::string 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");
|
||||
}
|
||||
fileTree.put("username", newUsername);
|
||||
fileTree.put("password", util::hex(crypto::hash(newPassword + config::sunshine.salt)).to_string());
|
||||
fileTree.put("salt", config::sunshine.salt);
|
||||
pt::write_json(config::sunshine.credentials_file, fileTree);
|
||||
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) {
|
||||
BOOST_LOG(warning) << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void savePin(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
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::read_json(ss, inputTree);
|
||||
std::string pin = inputTree.get<std::string>("pin");
|
||||
outputTree.put("status", nvhttp::pin(pin));
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
|
||||
ctx->use_certificate_chain_file(config::nvhttp.cert);
|
||||
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
|
||||
https_server_t server { ctx, 0 };
|
||||
server.default_resource = not_found;
|
||||
server.resource["^/$"]["GET"] = getIndexPage;
|
||||
server.resource["^/pin$"]["GET"] = getPinPage;
|
||||
server.resource["^/apps$"]["GET"] = getAppsPage;
|
||||
server.resource["^/clients$"]["GET"] = getClientsPage;
|
||||
server.resource["^/config$"]["GET"] = getConfigPage;
|
||||
server.resource["^/password$"]["GET"] = getPasswordPage;
|
||||
server.resource["^/api/pin"]["POST"] = savePin;
|
||||
server.resource["^/api/apps$"]["GET"] = getApps;
|
||||
server.resource["^/api/apps$"]["POST"] = saveApp;
|
||||
server.resource["^/api/config$"]["GET"] = getConfig;
|
||||
server.resource["^/api/config$"]["POST"] = saveConfig;
|
||||
server.resource["^/api/password$"]["POST"] = savePassword;
|
||||
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
|
||||
server.config.reuse_address = true;
|
||||
server.config.address = "0.0.0.0"s;
|
||||
server.config.port = PORT_HTTP;
|
||||
|
||||
try {
|
||||
server.bind();
|
||||
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << PORT_HTTP << "]";
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << PORT_HTTP << "]: "sv << err.what();
|
||||
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
auto accept_and_run = [&](auto *server) {
|
||||
try {
|
||||
server->accept_and_run();
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
// It's possible the exception gets thrown after calling server->stop() from a different thread
|
||||
if(shutdown_event->peek()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(fatal) << "Couldn't start Configuration HTTP server to ports ["sv << PORT_HTTP << ", "sv << PORT_HTTP << "]: "sv << err.what();
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
std::thread tcp { accept_and_run, &server };
|
||||
|
||||
// Wait for any event
|
||||
shutdown_event->view();
|
||||
|
||||
server.stop();
|
||||
|
||||
tcp.join();
|
||||
}
|
||||
} // namespace confighttp
|
||||
|
||||
std::string read_file(std::string path) {
|
||||
std::ifstream in(path);
|
||||
|
||||
std::string input;
|
||||
std::string base64_cert;
|
||||
|
||||
//FIXME: Being unable to read file could result in infinite loop
|
||||
while(!in.eof()) {
|
||||
std::getline(in, input);
|
||||
base64_cert += input + '\n';
|
||||
}
|
||||
|
||||
return base64_cert;
|
||||
}
|
20
sunshine/confighttp.h
Normal file
20
sunshine/confighttp.h
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// Created by loki on 6/3/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_CONFIGHTTP_H
|
||||
#define SUNSHINE_CONFIGHTTP_H
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "thread_safe.h"
|
||||
|
||||
#define WEB_DIR SUNSHINE_ASSETS_DIR "/web/"
|
||||
|
||||
|
||||
namespace confighttp {
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event);
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_CONFIGHTTP_H
|
@ -3,9 +3,10 @@
|
||||
//
|
||||
|
||||
#include "crypto.h"
|
||||
#include <iostream>
|
||||
#include <openssl/pem.h>
|
||||
namespace crypto {
|
||||
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
|
||||
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
|
||||
//using rsa_t = util::safe_ptr<RSA, RSA_free>;
|
||||
using asn1_string_t = util::safe_ptr<ASN1_STRING, ASN1_STRING_free>;
|
||||
|
||||
@ -338,4 +339,14 @@ bool verify256(const x509_t &x509, const std::string_view &data, const std::stri
|
||||
void md_ctx_destroy(EVP_MD_CTX *ctx) {
|
||||
EVP_MD_CTX_destroy(ctx);
|
||||
}
|
||||
} // namespace crypto
|
||||
|
||||
std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet) {
|
||||
auto value = rand(bytes);
|
||||
|
||||
for(std::size_t i = 0; i != value.size(); ++i) {
|
||||
value[i] = alphabet[value[i] % alphabet.length()];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace crypto
|
@ -7,6 +7,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <iomanip>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
@ -35,6 +36,7 @@ using bio_t = util::safe_ptr<BIO, BIO_free_all>;
|
||||
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
|
||||
|
||||
sha256_t hash(const std::string_view &plaintext);
|
||||
|
||||
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin);
|
||||
|
||||
x509_t x509(const std::string_view &x);
|
||||
@ -50,6 +52,8 @@ creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits);
|
||||
std::string_view signature(const x509_t &x);
|
||||
|
||||
std::string rand(std::size_t bytes);
|
||||
std::string rand_alphabet(std::size_t bytes,
|
||||
const std::string_view &alphabet = std::string_view { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-" });
|
||||
|
||||
class cert_chain_t {
|
||||
public:
|
||||
|
183
sunshine/httpcommon.cpp
Normal file
183
sunshine/httpcommon.cpp
Normal file
@ -0,0 +1,183 @@
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <Simple-Web-Server/server_http.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
namespace http {
|
||||
using namespace std::literals;
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
int create_creds(const std::string &pkey, const std::string &cert);
|
||||
int generate_user_creds(const std::string &file);
|
||||
int reload_user_creds(const std::string &file);
|
||||
std::string read_file(const char *path);
|
||||
int write_file(const char *path, const std::string_view &contents);
|
||||
std::string unique_id;
|
||||
net::net_e origin_pin_allowed;
|
||||
|
||||
void init(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
|
||||
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
|
||||
if(clean_slate) {
|
||||
unique_id = util::uuid_t::generate().string();
|
||||
auto dir = std::filesystem::temp_directory_path() / "Sushine"sv;
|
||||
config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string();
|
||||
config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string();
|
||||
}
|
||||
|
||||
if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
|
||||
if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!fs::exists(config::sunshine.credentials_file)) {
|
||||
if(generate_user_creds(config::sunshine.credentials_file)) {
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(reload_user_creds(config::sunshine.credentials_file)) {
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int generate_user_creds(const std::string &file) {
|
||||
pt::ptree outputTree;
|
||||
try {
|
||||
std::string username = "sunshine";
|
||||
std::string plainPassword = crypto::rand_alphabet(16);
|
||||
std::string salt = crypto::rand_alphabet(16);
|
||||
outputTree.put("username", "sunshine");
|
||||
outputTree.put("salt", salt);
|
||||
outputTree.put("password", util::hex(crypto::hash(plainPassword + salt)).to_string());
|
||||
BOOST_LOG(info) << "New credentials have been created";
|
||||
BOOST_LOG(info) << "Username: " << username;
|
||||
BOOST_LOG(info) << "Password: " << plainPassword;
|
||||
pt::write_json(file, outputTree);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(fatal) << e.what();
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int reload_user_creds(const std::string &file) {
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
pt::read_json(file, inputTree);
|
||||
config::sunshine.username = inputTree.get<std::string>("username");
|
||||
config::sunshine.password = inputTree.get<std::string>("password");
|
||||
config::sunshine.salt = inputTree.get<std::string>("salt");
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(fatal) << e.what();
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int create_creds(const std::string &pkey, const std::string &cert) {
|
||||
fs::path pkey_path = pkey;
|
||||
fs::path cert_path = cert;
|
||||
|
||||
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
|
||||
|
||||
auto pkey_dir = pkey_path;
|
||||
auto cert_dir = cert_path;
|
||||
pkey_dir.remove_filename();
|
||||
cert_dir.remove_filename();
|
||||
|
||||
std::error_code err_code {};
|
||||
fs::create_directories(pkey_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::create_directories(cert_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(pkey.c_str(), creds.pkey)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(cert.c_str(), creds.x509)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.cert << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(pkey_path,
|
||||
fs::perms::owner_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(cert_path,
|
||||
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
int write_file(const char *path, const std::string_view &contents) {
|
||||
std::ofstream out(path);
|
||||
|
||||
if(!out.is_open()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out << contents;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string read_file(const char *path) {
|
||||
std::ifstream in(path);
|
||||
|
||||
std::string input;
|
||||
std::string base64_cert;
|
||||
|
||||
//FIXME: Being unable to read file could result in infinite loop
|
||||
while(!in.eof()) {
|
||||
std::getline(in, input);
|
||||
base64_cert += input + '\n';
|
||||
}
|
||||
|
||||
return base64_cert;
|
||||
}
|
||||
} // namespace http
|
14
sunshine/httpcommon.h
Normal file
14
sunshine/httpcommon.h
Normal file
@ -0,0 +1,14 @@
|
||||
#include "network.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
namespace http {
|
||||
|
||||
void init(std::shared_ptr<safe::signal_t> shutdown_event);
|
||||
int create_creds(const std::string &pkey, const std::string &cert);
|
||||
std::string read_file(const char *path);
|
||||
int write_file(const char *path, const std::string_view &contents);
|
||||
int reload_user_creds(const std::string &file);
|
||||
extern std::string unique_id;
|
||||
extern net::net_e origin_pin_allowed;
|
||||
|
||||
} // namespace http
|
@ -17,6 +17,8 @@
|
||||
#include <boost/log/sources/severity_logger.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
#include "httpcommon.h"
|
||||
#include "nvhttp.h"
|
||||
#include "rtsp.h"
|
||||
#include "thread_pool.h"
|
||||
@ -128,18 +130,7 @@ int main(int argc, char *argv[]) {
|
||||
shutdown_event->raise(true);
|
||||
});
|
||||
|
||||
auto proc_opt = proc::parse(config::stream.file_apps);
|
||||
if(!proc_opt) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
ctx.name = "Desktop"s;
|
||||
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
|
||||
}
|
||||
|
||||
proc::proc = std::move(*proc_opt);
|
||||
proc::refresh(config::stream.file_apps);
|
||||
|
||||
auto deinit_guard = platf::init();
|
||||
if(!deinit_guard) {
|
||||
@ -150,13 +141,16 @@ int main(int argc, char *argv[]) {
|
||||
if(video::init()) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
http::init(shutdown_event);
|
||||
task_pool.start(1);
|
||||
|
||||
std::thread httpThread { nvhttp::start, shutdown_event };
|
||||
std::thread configThread { confighttp::start, shutdown_event };
|
||||
stream::rtpThread(shutdown_event);
|
||||
|
||||
httpThread.join();
|
||||
configThread.join();
|
||||
|
||||
task_pool.stop();
|
||||
task_pool.join();
|
||||
|
||||
|
@ -18,8 +18,10 @@
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
@ -28,7 +30,6 @@
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
|
||||
namespace nvhttp {
|
||||
using namespace std::literals;
|
||||
constexpr auto PORT_HTTP = 47989;
|
||||
@ -77,8 +78,6 @@ struct pair_session_t {
|
||||
// uniqueID, session
|
||||
std::unordered_map<std::string, pair_session_t> map_id_sess;
|
||||
std::unordered_map<std::string, client_t> map_id_client;
|
||||
std::string unique_id;
|
||||
net::net_e origin_pin_allowed;
|
||||
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
||||
@ -94,7 +93,7 @@ enum class op_e {
|
||||
void save_state() {
|
||||
pt::ptree root;
|
||||
|
||||
root.put("root.uniqueid", unique_id);
|
||||
root.put("root.uniqueid", http::unique_id);
|
||||
auto &nodes = root.add_child("root.devices", pt::ptree {});
|
||||
for(auto &[_, client] : map_id_client) {
|
||||
pt::ptree node;
|
||||
@ -116,8 +115,9 @@ void save_state() {
|
||||
}
|
||||
|
||||
void load_state() {
|
||||
if(!fs::exists(config::nvhttp.file_state)) {
|
||||
unique_id = util::uuid_t::generate().string();
|
||||
auto file_state = fs::current_path() / config::nvhttp.file_state;
|
||||
if(!fs::exists(file_state)) {
|
||||
http::unique_id = util::uuid_t::generate().string();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ void load_state() {
|
||||
return;
|
||||
}
|
||||
|
||||
unique_id = root.get<std::string>("root.uniqueid");
|
||||
http::unique_id = root.get<std::string>("root.uniqueid");
|
||||
auto device_nodes = root.get_child("root.devices");
|
||||
|
||||
for(auto &[_, device_node] : device_nodes) {
|
||||
@ -419,30 +419,14 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
|
||||
response->write(data.str());
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
print_req<T>(request);
|
||||
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
if(ip_type > origin_pin_allowed) {
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool pin(std::string pin) {
|
||||
pt::ptree tree;
|
||||
|
||||
if(map_id_sess.empty()) {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &sess = std::begin(map_id_sess)->second;
|
||||
getservercert(sess, tree, request->path_match[1]);
|
||||
getservercert(sess, tree, pin);
|
||||
|
||||
// response to the request for pin
|
||||
std::ostringstream data;
|
||||
@ -456,15 +440,36 @@ void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response,
|
||||
async_response.right()->write(data.str());
|
||||
}
|
||||
else {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// reset async_response
|
||||
async_response = std::decay_t<decltype(async_response.left())>();
|
||||
// response to the current request
|
||||
response->write(SimpleWeb::StatusCode::success_ok);
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
print_req<T>(request);
|
||||
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
if(ip_type > http::origin_pin_allowed) {
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool pinResponse = pin(request->path_match[1]);
|
||||
if(pinResponse) {
|
||||
response->write(SimpleWeb::StatusCode::success_ok);
|
||||
}
|
||||
else {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
@ -491,7 +496,7 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
|
||||
tree.put("root.appversion", VERSION);
|
||||
tree.put("root.GfeVersion", GFE_VERSION);
|
||||
tree.put("root.uniqueid", unique_id);
|
||||
tree.put("root.uniqueid", http::unique_id);
|
||||
tree.put("root.mac", platf::get_mac_address(request->local_endpoint_address()));
|
||||
tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0");
|
||||
tree.put("root.LocalIP", request->local_endpoint_address());
|
||||
@ -728,88 +733,16 @@ void appasset(resp_https_t response, req_https_t request) {
|
||||
response->write(SimpleWeb::StatusCode::success_ok, in);
|
||||
}
|
||||
|
||||
int create_creds(const std::string &pkey, const std::string &cert) {
|
||||
fs::path pkey_path = pkey;
|
||||
fs::path cert_path = cert;
|
||||
|
||||
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
|
||||
|
||||
auto pkey_dir = pkey_path;
|
||||
auto cert_dir = cert_path;
|
||||
pkey_dir.remove_filename();
|
||||
cert_dir.remove_filename();
|
||||
|
||||
std::error_code err_code {};
|
||||
fs::create_directories(pkey_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::create_directories(cert_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(pkey.c_str(), creds.pkey)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(cert.c_str(), creds.x509)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.cert << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(pkey_path,
|
||||
fs::perms::owner_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(cert_path,
|
||||
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
|
||||
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
|
||||
if(clean_slate) {
|
||||
unique_id = util::uuid_t::generate().string();
|
||||
|
||||
auto dir = std::filesystem::temp_directory_path() / "Sushine"sv;
|
||||
|
||||
config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string();
|
||||
config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string();
|
||||
}
|
||||
|
||||
|
||||
if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
|
||||
if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
|
||||
|
||||
if(!clean_slate) {
|
||||
load_state();
|
||||
}
|
||||
|
||||
conf_intern.pkey = read_file(config::nvhttp.pkey.c_str());
|
||||
conf_intern.servercert = read_file(config::nvhttp.cert.c_str());
|
||||
conf_intern.pkey = http::read_file(config::nvhttp.pkey.c_str());
|
||||
conf_intern.servercert = http::read_file(config::nvhttp.cert.c_str());
|
||||
|
||||
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
|
||||
ctx->use_certificate_chain_file(config::nvhttp.cert);
|
||||
|
@ -7,7 +7,8 @@
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <Simple-Web-Server/server_http.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include "thread_safe.h"
|
||||
|
||||
#define CA_DIR SUNSHINE_ASSETS_DIR "/demoCA"
|
||||
@ -16,6 +17,7 @@
|
||||
|
||||
namespace nvhttp {
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event);
|
||||
}
|
||||
bool pin(std::string pin);
|
||||
} // namespace nvhttp
|
||||
|
||||
#endif //SUNSHINE_NVHTTP_H
|
||||
|
@ -332,6 +332,11 @@ void refresh(const std::string &file_name) {
|
||||
auto proc_opt = proc::parse(file_name);
|
||||
|
||||
if(proc_opt) {
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
ctx.name = "Desktop"s;
|
||||
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
|
||||
}
|
||||
proc = std::move(*proc_opt);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user