mirror of
https://github.com/badaix/snapcast.git
synced 2025-06-02 10:51:45 +02:00
add simple webserver
jsonrpc is available for HTTP post: http://<your-ip>:8080/jsonrpc for websockets: ws://<your-ip>:8080/jsonrpc
This commit is contained in:
parent
ade48cf5e4
commit
fa508eafba
4 changed files with 699 additions and 29 deletions
551
control/interface.html
Normal file
551
control/interface.html
Normal file
|
@ -0,0 +1,551 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>Snapcast Interface</title>
|
||||
Taken from <a href="https://github.com/derglaus/snapcast-websockets-ui">derglaus/snapcast-websockets-ui</a> for testing purposes
|
||||
<script>
|
||||
var connection = new WebSocket('ws://127.0.0.1:8080/jsonrpc');
|
||||
var server;
|
||||
|
||||
connection.onmessage = function (e) {
|
||||
var recv = e.data;
|
||||
//String.fromCharCode.apply(null, new Uint8Array(e.data));
|
||||
console.log(recv);
|
||||
var answer = JSON.parse(recv);
|
||||
if (answer.id == 1) { server = answer.result; }
|
||||
// console.log(answer.method);
|
||||
if (answer.method == "Client.OnVolumeChanged" || answer.method == "Client.OnLatencyChanged" || answer.method == "Client.OnNameChanged") { clientChange(answer.params); }
|
||||
if (answer.method == "Client.OnConnect" || answer.method == "Client.OnDisconnect") { clientConnect(answer.params); }
|
||||
if (answer.method == "Group.OnMute") { groupMute(answer.params); }
|
||||
if (answer.method == "Group.OnStreamChanged") { groupStream(answer.params); }
|
||||
if (answer.method == "Stream.OnUpdate") { streamUpdate(answer.params); }
|
||||
if (answer.method == "Server.OnUpdate") { server = answer.params }
|
||||
show()
|
||||
}
|
||||
|
||||
connection.onopen = function () {
|
||||
send('{"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"}')
|
||||
}
|
||||
|
||||
connection.onerror = function () {
|
||||
alert("error");
|
||||
}
|
||||
|
||||
function send(str) {
|
||||
connection.send(str)
|
||||
}
|
||||
|
||||
function clientChange(params) {//console.log(params);
|
||||
var i_group = 0
|
||||
while (i_group < server.server.groups.length) {
|
||||
var i_client = 0
|
||||
while (i_client < server.server.groups[i_group].clients.length) {
|
||||
if (server.server.groups[i_group].clients[i_client].id == params.id) {//console.log(server.server.groups[i_group].clients[i_client]);
|
||||
server.server.groups[i_group].clients[i_client].config = Object.assign(server.server.groups[i_group].clients[i_client].config, params);
|
||||
//console.log(server.server.groups[i_group].clients[i_client]);
|
||||
}
|
||||
i_client++
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
}
|
||||
|
||||
function clientConnect(params) {//console.log(params);
|
||||
var i_group = 0
|
||||
|
||||
while (i_group < server.server.groups.length) {
|
||||
var i_client = 0
|
||||
while (i_client < server.server.groups[i_group].clients.length) {
|
||||
if (server.server.groups[i_group].clients[i_client].id == params.client.id) {
|
||||
server.server.groups[i_group].clients[i_client] = params.client;
|
||||
// console.log(server.server.groups[i_group].clients[i_client]);
|
||||
}
|
||||
i_client++
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
}
|
||||
|
||||
function groupMute(params) {//console.log(params);
|
||||
var i_group = 0
|
||||
|
||||
while (i_group < server.server.groups.length) {
|
||||
if (server.server.groups[i_group].id == params.id) {
|
||||
server.server.groups[i_group].muted = params.mute;
|
||||
// console.log(server.server.groups[i_group]);
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
}
|
||||
|
||||
function groupStream(params) {//console.log(params);
|
||||
var i_group = 0
|
||||
|
||||
while (i_group < server.server.groups.length) {
|
||||
if (server.server.groups[i_group].id == params.id) {
|
||||
server.server.groups[i_group].stream_id = params.stream_id;
|
||||
// console.log(server.server.groups[i_group]);
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
}
|
||||
|
||||
function streamUpdate(params) {//console.log(params);
|
||||
var i_stream = 0
|
||||
|
||||
while (i_stream < server.server.streams.length) {
|
||||
if (server.server.streams[i_stream].id == params.id) {
|
||||
server.server.streams[i_stream] = params.stream;
|
||||
// console.log(server.server.streams[i_stream]);
|
||||
}
|
||||
i_stream++
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
var i_group = 0;
|
||||
var content = "";
|
||||
|
||||
while (i_group < server.server.groups.length) {
|
||||
var i_client = 0;
|
||||
var unmuted;
|
||||
var streamselect = "<select id='stream_" + server.server.groups[i_group].id + "' onchange='setStream(\"" + server.server.groups[i_group].id + "\")' class='stream'>"
|
||||
|
||||
var i_stream = 0;
|
||||
while (i_stream < server.server.streams.length) {
|
||||
var streamselected = "";
|
||||
if (server.server.groups[i_group].stream_id == server.server.streams[i_stream].id) { streamselected = 'selected' }
|
||||
streamselect = streamselect + "<option value='" + server.server.streams[i_stream].id + "' " + streamselected + ">" + server.server.streams[i_stream].id + ": " + server.server.streams[i_stream].status + "</option>";
|
||||
i_stream++
|
||||
}
|
||||
streamselect = streamselect + "</select>";
|
||||
var classgroup = 'group';
|
||||
if (server.server.groups[i_group].muted == true) { classgroup = 'groupmuted' }
|
||||
content = content + "<div id='g_" + server.server.groups[i_group].id + "' class='" + classgroup + "'>";
|
||||
content = content + streamselect;
|
||||
|
||||
var mutetext;
|
||||
|
||||
if (server.server.groups[i_group].muted == true) {
|
||||
unmuted = 'false';
|
||||
mutetext = '🔇';
|
||||
}
|
||||
if (server.server.groups[i_group].muted == false) {
|
||||
unmuted = 'true';
|
||||
mutetext = '🔊';
|
||||
}
|
||||
|
||||
content = content + " <a href=\"javascript:setMuteGroup('" + server.server.groups[i_group].id + "','" + unmuted + "');\" class='mutebuttongroup'>" + mutetext + "</a>";
|
||||
//content=content+": "+server.server.groups[i_group].muted;
|
||||
|
||||
content = content + "<input type='button' value='Refresh' class='refreshbutton' onclick='javascript: location.reload()'>";
|
||||
content = content + "<br>";
|
||||
while (i_client < server.server.groups[i_group].clients.length) {
|
||||
var sv = server.server.groups[i_group].clients[i_client];
|
||||
|
||||
var groupselect = "<select id='group_" + sv.id + "' onchange='setGroup(\"" + sv.id + "\")'>";
|
||||
|
||||
var o_group = 0
|
||||
while (o_group < server.server.groups.length) {
|
||||
var groupselected = "";
|
||||
if (o_group == i_group) { groupselected = 'selected' }
|
||||
|
||||
groupselect = groupselect + "<option value='" + server.server.groups[o_group].id + "' " + groupselected + ">Group " + o_group + " (" + server.server.groups[o_group].clients.length + " Clients)</option>";
|
||||
o_group++
|
||||
}
|
||||
groupselect = groupselect + "<option value='new'>new</option>";
|
||||
groupselect = groupselect + "</select>"
|
||||
|
||||
var name;
|
||||
var unmuted;
|
||||
if (sv.config.name != "") { name = sv.config.name; }
|
||||
else { name = sv.host.name; }
|
||||
|
||||
var clas = 'client'
|
||||
if (sv.connected == false) { clas = 'disconnected'; }
|
||||
|
||||
content = content + "<div id='c_" + sv.id + "' class='" + clas + "'>";
|
||||
|
||||
var mutetextclient;
|
||||
if (sv.config.volume.muted == true) {
|
||||
unmuted = 'false';
|
||||
mutetext = '🔇';
|
||||
}
|
||||
if (sv.config.volume.muted == false) {
|
||||
unmuted = 'true';
|
||||
mutetext = '🔊';
|
||||
}
|
||||
content = content + " <a href=\"javascript:setVolume('" + sv.id + "','" + unmuted + "');\" class='mutebutton'>" + mutetext + "</a>";
|
||||
// content=content+": "+sv.config.volume.muted;
|
||||
|
||||
var sliderclass = 'slider';
|
||||
if (sv.config.volume.muted == true) { sliderclass = 'slidermute'; }
|
||||
|
||||
content = content + "<div class='sliders'><div class='sliderdiv'><input type='range' min=0 max=100 step=1 id='vol_" + sv.id + "' onchange='javascript:setVolume(\"" + sv.id + "\",\"" + sv.config.volume.muted + "\")' value=" + sv.config.volume.percent + " class='" + sliderclass + "' orient='vertical'></div>";
|
||||
content = content + "<div class='finebg'>++<br>+<br>0<br>-<br>--</div><div class='sliderdiv_fine'><input type='range' min=0 max=10 step=1 id='vol_fine_" + sv.id + "' onchange='javascript:setVolume(\"" + sv.id + "\",\"" + sv.config.volume.muted + "\")' value=5 class='" + sliderclass + "_fine' orient='vertical'></div></div>";
|
||||
content = content + " <a href=\"javascript:setName('" + sv.id + "');\" class='edit'>✎</a>";
|
||||
content = content + name;
|
||||
// content=content+" Connected:"+sv.connected;
|
||||
content = content + groupselect;
|
||||
content = content + "</div>";
|
||||
|
||||
i_client++
|
||||
}
|
||||
content = content + "</div>"
|
||||
i_group++
|
||||
}
|
||||
|
||||
content = content + "<br><br>";
|
||||
document.getElementById('show').innerHTML = content;
|
||||
}
|
||||
|
||||
function setVolume(id, mute) {
|
||||
percent = document.getElementById('vol_' + id).value;
|
||||
percent_fine = document.getElementById('vol_fine_' + id).value;
|
||||
|
||||
//alert(percent +" "+percent_fine+" "+Number(percent));
|
||||
percent = Number(percent) + Number(percent_fine) - 5;
|
||||
if (percent < 0) { percent = 0 }
|
||||
if (percent > 100) { percent = 100 }
|
||||
|
||||
send('{"id":8,"jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"' + id + '","volume":{"muted":' + mute + ',"percent":' + percent + '}}}')
|
||||
|
||||
var i_group = 0
|
||||
|
||||
while (i_group < server.server.groups.length) {
|
||||
var i_client = 0
|
||||
while (i_client < server.server.groups[i_group].clients.length) {
|
||||
var sv = server.server.groups[i_group].clients[i_client];
|
||||
|
||||
if (sv.id == id) {
|
||||
if (mute == 'true') { sv.config.volume.muted = true; }
|
||||
if (mute == 'false') { sv.config.volume.muted = false; }
|
||||
sv.config.volume.percent = percent;
|
||||
// console.log(server.server.groups[i_group]);
|
||||
}
|
||||
|
||||
i_client++
|
||||
}
|
||||
|
||||
i_group++
|
||||
}
|
||||
|
||||
show()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function setMuteGroup(id, what) {
|
||||
send('{"id":"MuteGroup_' + id + '","jsonrpc":"2.0","method":"Group.SetMute","params":{"id":"' + id + '","mute":' + what + '}}')
|
||||
var i_group = 0
|
||||
while (i_group < server.server.groups.length) {
|
||||
if (server.server.groups[i_group].id == id) {
|
||||
if (what == 'true') { server.server.groups[i_group].muted = true; }
|
||||
if (what == 'false') { server.server.groups[i_group].muted = false; }
|
||||
// console.log(server.server.groups[i_group]);
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
show()
|
||||
}
|
||||
|
||||
function setStream(id) {
|
||||
|
||||
send('{"id":4,"jsonrpc":"2.0","method":"Group.SetStream","params":{"id":"' + id + '","stream_id":"' + document.getElementById('stream_' + id).value + '"}}')
|
||||
|
||||
var i_group = 0
|
||||
|
||||
while (i_group < server.server.groups.length) {
|
||||
if (server.server.groups[i_group].id == id) {
|
||||
server.server.groups[i_group].stream_id = document.getElementById('stream_' + id).value;
|
||||
// console.log(server.server.groups[i_group]);
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
show()
|
||||
}
|
||||
|
||||
function setGroup(id) {
|
||||
group = document.getElementById('group_' + id).value;
|
||||
var current_group;
|
||||
var i_group = 0
|
||||
while (i_group < server.server.groups.length) {
|
||||
var i_client = 0
|
||||
while (i_client < server.server.groups[i_group].clients.length) {
|
||||
if (id == server.server.groups[i_group].clients[i_client].id) { current_group = server.server.groups[i_group].id }
|
||||
i_client++
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
|
||||
var send_clients = [];
|
||||
var i_group = 0
|
||||
while (i_group < server.server.groups.length) {
|
||||
if (server.server.groups[i_group].id == group || (group == "new" && server.server.groups[i_group].id == current_group)) {
|
||||
var i_client = 0
|
||||
while (i_client < server.server.groups[i_group].clients.length) {
|
||||
if (group == "new" && server.server.groups[i_group].clients[i_client].id == id) { }
|
||||
else {//console.log(group);
|
||||
//console.log(server.server.groups[i_group].clients[i_client].id);
|
||||
//console.log(id);
|
||||
send_clients[send_clients.length] = server.server.groups[i_group].clients[i_client].id;
|
||||
}
|
||||
i_client++
|
||||
}
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
if (group != "new") { send_clients[send_clients.length] = id; }
|
||||
|
||||
var send_clients_string = JSON.stringify(send_clients);
|
||||
// console.log(send_clients_string);
|
||||
|
||||
var sendgroup = group
|
||||
if (group == "new") { group = current_group }
|
||||
|
||||
send('{"id":1,"jsonrpc":"2.0","method":"Group.SetClients","params":{"clients":' + send_clients_string + ',"id":"' + group + '"}}')
|
||||
//send('{"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"}}')
|
||||
}
|
||||
|
||||
function setName(id) {
|
||||
var current_name;
|
||||
var current_latemcy;
|
||||
var i_group = 0;
|
||||
|
||||
while (i_group < server.server.groups.length) {
|
||||
var i_client = 0
|
||||
while (i_client < server.server.groups[i_group].clients.length) {
|
||||
var sv = server.server.groups[i_group].clients[i_client];
|
||||
if (sv.id == id) {
|
||||
if (sv.config.name != "") { current_name = sv.config.name; }
|
||||
else { current_name = sv.host.name; }
|
||||
current_latency = sv.config.latency;
|
||||
}
|
||||
i_client++
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
|
||||
var newName = window.prompt("New Name", current_name);
|
||||
var newLatency = window.prompt("New Latency", current_latency);
|
||||
|
||||
send('{"id":6,"jsonrpc":"2.0","method":"Client.SetName","params":{"id":"' + id + '","name":"' + newName + '"}}')
|
||||
send('{"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"' + id + '","latency":' + newLatency + '}}')
|
||||
|
||||
var i_group = 0;
|
||||
while (i_group < server.server.groups.length) {
|
||||
var i_client = 0
|
||||
while (i_client < server.server.groups[i_group].clients.length) {
|
||||
var sv = server.server.groups[i_group].clients[i_client];
|
||||
|
||||
if (sv.id == id) {
|
||||
sv.config.name = newName;
|
||||
sv.config.latency = newLatency;
|
||||
}
|
||||
i_client++
|
||||
}
|
||||
i_group++
|
||||
}
|
||||
show()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: #1f1f1f;
|
||||
color: rgb(255, 255, 255);
|
||||
font-family: 'Arial', sans-serif;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1f1f1f;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.group {
|
||||
float: none;
|
||||
clear: both;
|
||||
margin: 20px 15px 10px 15px;
|
||||
border: 2px solid #5f5f5f;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.groupmuted {
|
||||
float: none;
|
||||
clear: both;
|
||||
margin: 20px 15px 10px 15px;
|
||||
border: 2px solid #5f5f5f;
|
||||
overflow: auto;
|
||||
opacity: 0.27;
|
||||
}
|
||||
|
||||
.client {
|
||||
text-align: center;
|
||||
background: rgb(61, 61, 61);
|
||||
margin: 10px;
|
||||
width: 160px;
|
||||
height: 360px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.disconnected {
|
||||
text-align: center;
|
||||
background: rgb(61, 61, 61);
|
||||
margin: 10px;
|
||||
width: 160px;
|
||||
height: 360px;
|
||||
float: left;
|
||||
opacity: 0.27;
|
||||
}
|
||||
|
||||
.slider {
|
||||
writing-mode: bt-lr;
|
||||
/* IE */
|
||||
-webkit-appearance: slider-vertical;
|
||||
/* WebKit */
|
||||
height: 240px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.slidermute {
|
||||
writing-mode: bt-lr;
|
||||
/* IE */
|
||||
-webkit-appearance: slider-vertical;
|
||||
/* WebKit */
|
||||
opacity: 0.27;
|
||||
height: 240px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.slider_fine {
|
||||
writing-mode: bt-lr;
|
||||
/* IE */
|
||||
-webkit-appearance: slider-vertical;
|
||||
/* WebKit */
|
||||
height: 240px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.slidermute_fine {
|
||||
writing-mode: bt-lr;
|
||||
/* IE */
|
||||
-webkit-appearance: slider-vertical;
|
||||
/* WebKit */
|
||||
height: 240px;
|
||||
width: 15px;
|
||||
opacity: 0.27;
|
||||
}
|
||||
|
||||
|
||||
.sliders {
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
height: 250px;
|
||||
padding-top: 10px;
|
||||
clear: both;
|
||||
float: none;
|
||||
|
||||
}
|
||||
|
||||
.sliderdiv {
|
||||
display: inline-block;
|
||||
|
||||
padding-left: 40px;
|
||||
text-align: left;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.sliderdiv_fine {
|
||||
display: inline-block;
|
||||
|
||||
text-align: left;
|
||||
width: 20px;
|
||||
|
||||
position: relative;
|
||||
top: 0px;
|
||||
left: 13px;
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
.finebg {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-size: 35px;
|
||||
width: 40px;
|
||||
position: relative;
|
||||
top: -27px;
|
||||
left: 40px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: rgb(61, 61, 61);
|
||||
width: 150px;
|
||||
font-size: 20px;
|
||||
color: #e3e3e3;
|
||||
border: 1px solid #555555;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearancce: none;
|
||||
|
||||
}
|
||||
|
||||
.stream {
|
||||
margin-left: 20px;
|
||||
width: 200px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.refreshbutton {
|
||||
background-color: rgb(61, 61, 61);
|
||||
|
||||
font-size: 20px;
|
||||
color: #e3e3e3;
|
||||
border: 1px solid #555555;
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.mutebutton {
|
||||
color: #e3e3e3;
|
||||
font-size: 30px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mutebuttongroup {
|
||||
font-size: 30px;
|
||||
color: #e3e3e3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.edit {
|
||||
color: #e3e3e3;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="show"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -179,7 +179,9 @@ std::pair<acceptor_ptr, acceptor_ptr> ControlServer::createAcceptors(size_t port
|
|||
|
||||
void ControlServer::start()
|
||||
{
|
||||
// TODO: should be possible to be disabled
|
||||
acceptor_tcp_ = createAcceptors(port_);
|
||||
// TODO: make port configurable, should be possible to be disabled
|
||||
acceptor_http_ = createAcceptors(8080);
|
||||
startAccept();
|
||||
}
|
||||
|
|
|
@ -24,6 +24,79 @@
|
|||
|
||||
using namespace std;
|
||||
|
||||
static constexpr const char* HTTP_SERVER_NAME = "Snapcast";
|
||||
|
||||
namespace
|
||||
{
|
||||
// Return a reasonable mime type based on the extension of a file.
|
||||
boost::beast::string_view mime_type(boost::beast::string_view path)
|
||||
{
|
||||
using boost::beast::iequals;
|
||||
auto const ext = [&path] {
|
||||
auto const pos = path.rfind(".");
|
||||
if (pos == boost::beast::string_view::npos)
|
||||
return boost::beast::string_view{};
|
||||
return path.substr(pos);
|
||||
}();
|
||||
if (iequals(ext, ".htm"))
|
||||
return "text/html";
|
||||
if (iequals(ext, ".html"))
|
||||
return "text/html";
|
||||
if (iequals(ext, ".php"))
|
||||
return "text/html";
|
||||
if (iequals(ext, ".css"))
|
||||
return "text/css";
|
||||
if (iequals(ext, ".txt"))
|
||||
return "text/plain";
|
||||
if (iequals(ext, ".js"))
|
||||
return "application/javascript";
|
||||
if (iequals(ext, ".json"))
|
||||
return "application/json";
|
||||
if (iequals(ext, ".xml"))
|
||||
return "application/xml";
|
||||
if (iequals(ext, ".swf"))
|
||||
return "application/x-shockwave-flash";
|
||||
if (iequals(ext, ".flv"))
|
||||
return "video/x-flv";
|
||||
if (iequals(ext, ".png"))
|
||||
return "image/png";
|
||||
if (iequals(ext, ".jpe"))
|
||||
return "image/jpeg";
|
||||
if (iequals(ext, ".jpeg"))
|
||||
return "image/jpeg";
|
||||
if (iequals(ext, ".jpg"))
|
||||
return "image/jpeg";
|
||||
if (iequals(ext, ".gif"))
|
||||
return "image/gif";
|
||||
if (iequals(ext, ".bmp"))
|
||||
return "image/bmp";
|
||||
if (iequals(ext, ".ico"))
|
||||
return "image/vnd.microsoft.icon";
|
||||
if (iequals(ext, ".tiff"))
|
||||
return "image/tiff";
|
||||
if (iequals(ext, ".tif"))
|
||||
return "image/tiff";
|
||||
if (iequals(ext, ".svg"))
|
||||
return "image/svg+xml";
|
||||
if (iequals(ext, ".svgz"))
|
||||
return "image/svg+xml";
|
||||
return "application/text";
|
||||
}
|
||||
|
||||
// Append an HTTP rel-path to a local filesystem path.
|
||||
// The returned path is normalized for the platform.
|
||||
std::string path_cat(boost::beast::string_view base, boost::beast::string_view path)
|
||||
{
|
||||
if (base.empty())
|
||||
return path.to_string();
|
||||
std::string result = base.to_string();
|
||||
char constexpr path_separator = '/';
|
||||
if (result.back() == path_separator)
|
||||
result.resize(result.size() - 1);
|
||||
result.append(path.data(), path.size());
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, tcp::socket&& socket) : ControlSession(receiver), socket_(std::move(socket))
|
||||
{
|
||||
|
@ -55,7 +128,8 @@ void ControlSessionHttp::handle_request(http::request<Body, http::basic_fields<A
|
|||
// Returns a bad request response
|
||||
auto const bad_request = [&req](boost::beast::string_view why) {
|
||||
http::response<http::string_body> res{http::status::bad_request, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
// TODO: Server: Snapcast/VERSION
|
||||
res.set(http::field::server, HTTP_SERVER_NAME);
|
||||
res.set(http::field::content_type, "text/html");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = why.to_string();
|
||||
|
@ -66,7 +140,7 @@ void ControlSessionHttp::handle_request(http::request<Body, http::basic_fields<A
|
|||
// Returns a not found response
|
||||
auto const not_found = [&req](boost::beast::string_view target) {
|
||||
http::response<http::string_body> res{http::status::not_found, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::server, HTTP_SERVER_NAME);
|
||||
res.set(http::field::content_type, "text/html");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = "The resource '" + target.to_string() + "' was not found.";
|
||||
|
@ -77,7 +151,7 @@ void ControlSessionHttp::handle_request(http::request<Body, http::basic_fields<A
|
|||
// Returns a server error response
|
||||
auto const server_error = [&req](boost::beast::string_view what) {
|
||||
http::response<http::string_body> res{http::status::internal_server_error, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::server, HTTP_SERVER_NAME);
|
||||
res.set(http::field::content_type, "text/html");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = "An error occurred: '" + what.to_string() + "'";
|
||||
|
@ -86,25 +160,70 @@ void ControlSessionHttp::handle_request(http::request<Body, http::basic_fields<A
|
|||
};
|
||||
|
||||
// Make sure we can handle the method
|
||||
if (req.method() != http::verb::post)
|
||||
if ((req.method() != http::verb::get) && (req.method() != http::verb::head) && (req.method() != http::verb::post))
|
||||
return send(bad_request("Unknown HTTP-method"));
|
||||
|
||||
// handle json rpc requests
|
||||
if (req.method() == http::verb::post)
|
||||
{
|
||||
if (req.target() != "/jsonrpc")
|
||||
return send(bad_request("Illegal request-target"));
|
||||
|
||||
string response = message_receiver_->onMessageReceived(this, req.body());
|
||||
http::response<http::string_body> res{http::status::ok, req.version()};
|
||||
res.set(http::field::server, HTTP_SERVER_NAME);
|
||||
res.set(http::field::content_type, "application/json");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = response;
|
||||
res.prepare_payload();
|
||||
return send(std::move(res));
|
||||
}
|
||||
|
||||
// Request path must be absolute and not contain "..".
|
||||
// if (req.target().empty() || req.target()[0] != '/' || req.target().find("..") != boost::beast::string_view::npos)
|
||||
// return send(bad_request("Illegal request-target"));
|
||||
if (req.target().empty() || req.target()[0] != '/' || req.target().find("..") != beast::string_view::npos)
|
||||
return send(bad_request("Illegal request-target"));
|
||||
|
||||
LOG(DEBUG) << "content type: " << req[beast::http::field::content_type] << "\n";
|
||||
LOG(DEBUG) << "body: " << req.body() << "\n";
|
||||
// TODO: configurable, enable/disable
|
||||
std::string doc_root = "../control";
|
||||
// Build the path to the requested file
|
||||
std::string path = path_cat(doc_root, req.target());
|
||||
if (req.target().back() == '/')
|
||||
path.append("index.html");
|
||||
|
||||
// TODO: error handling: bad request, ...
|
||||
string response = message_receiver_->onMessageReceived(this, req.body());
|
||||
LOG(DEBUG) << "path: " << path << "\n";
|
||||
// Attempt to open the file
|
||||
beast::error_code ec;
|
||||
http::file_body::value_type body;
|
||||
body.open(path.c_str(), beast::file_mode::scan, ec);
|
||||
|
||||
http::response<http::string_body> res{http::status::ok, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, "application/json");
|
||||
// Handle the case where the file doesn't exist
|
||||
if (ec == boost::system::errc::no_such_file_or_directory)
|
||||
return send(not_found(req.target()));
|
||||
|
||||
// Handle an unknown error
|
||||
if (ec)
|
||||
return send(server_error(ec.message()));
|
||||
|
||||
// Cache the size since we need it after the move
|
||||
auto const size = body.size();
|
||||
|
||||
// Respond to HEAD request
|
||||
if (req.method() == http::verb::head)
|
||||
{
|
||||
http::response<http::empty_body> res{http::status::ok, req.version()};
|
||||
res.set(http::field::server, HTTP_SERVER_NAME);
|
||||
res.set(http::field::content_type, mime_type(path));
|
||||
res.content_length(size);
|
||||
res.keep_alive(req.keep_alive());
|
||||
return send(std::move(res));
|
||||
}
|
||||
|
||||
// Respond to GET request
|
||||
http::response<http::file_body> res{std::piecewise_construct, std::make_tuple(std::move(body)), std::make_tuple(http::status::ok, req.version())};
|
||||
res.set(http::field::server, HTTP_SERVER_NAME);
|
||||
res.set(http::field::content_type, mime_type(path));
|
||||
res.content_length(size);
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = response; // R"({"jsonrpc": "2.0", "id": 1, "result": "stopped"})";
|
||||
res.prepare_payload();
|
||||
return send(std::move(res));
|
||||
}
|
||||
|
||||
|
@ -124,13 +243,11 @@ void ControlSessionHttp::on_read(beast::error_code ec, std::size_t bytes_transfe
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO: error handling
|
||||
// urls should be:
|
||||
// http://<host>/snapcast/rpc
|
||||
// ws://<host>/snapcast/ws or ws://<host>/snapcast/rpc?
|
||||
LOG(DEBUG) << "method: " << req_.method_string() << ", content type: " << req_[beast::http::field::content_type] << ", target: " << req_.target()
|
||||
<< ", body: " << req_.body() << "\n";
|
||||
|
||||
// See if it is a WebSocket Upgrade
|
||||
if (websocket::is_upgrade(req_))
|
||||
if (websocket::is_upgrade(req_) && (req_.target() == "/jsonrpc"))
|
||||
{
|
||||
// Create a WebSocket session by transferring the socket
|
||||
// std::make_shared<websocket_session>(std::move(socket_), state_)->run(std::move(req_));
|
||||
|
|
|
@ -63,18 +63,18 @@ int main(int argc, char* argv[])
|
|||
auto groffSwitch = op.add<Switch, Attribute::hidden>("", "groff", "produce groff message");
|
||||
auto debugOption = op.add<Implicit<string>, Attribute::hidden>("", "debug", "enable debug logging", "");
|
||||
auto versionSwitch = op.add<Switch>("v", "version", "Show version number");
|
||||
/*auto portValue =*/op.add<Value<size_t>>("p", "port", "Server port", settings.port, &settings.port);
|
||||
/*auto controlPortValue =*/op.add<Value<size_t>>("", "controlPort", "Remote control port", settings.controlPort, &settings.controlPort);
|
||||
op.add<Value<size_t>>("p", "port", "Server port", settings.port, &settings.port);
|
||||
op.add<Value<size_t>>("", "controlPort", "Remote control port", settings.controlPort, &settings.controlPort);
|
||||
auto streamValue = op.add<Value<string>>(
|
||||
"s", "stream", "URI of the PCM input stream.\nFormat: TYPE://host/path?name=NAME\n[&codec=CODEC]\n[&sampleformat=SAMPLEFORMAT]", pcmStream,
|
||||
&pcmStream);
|
||||
|
||||
/*auto sampleFormatValue =*/op.add<Value<string>>("", "sampleformat", "Default sample format", settings.sampleFormat, &settings.sampleFormat);
|
||||
/*auto codecValue =*/op.add<Value<string>>(
|
||||
"c", "codec", "Default transport codec\n(flac|ogg|pcm)[:options]\nType codec:? to get codec specific options", settings.codec, &settings.codec);
|
||||
/*auto streamBufferValue =*/op.add<Value<size_t>>("", "streamBuffer", "Default stream read buffer [ms]", settings.streamReadMs, &settings.streamReadMs);
|
||||
/*auto bufferValue =*/op.add<Value<int>>("b", "buffer", "Buffer [ms]", settings.bufferMs, &settings.bufferMs);
|
||||
/*auto muteSwitch =*/op.add<Switch>("", "sendToMuted", "Send audio to muted clients", &settings.sendAudioToMutedClients);
|
||||
op.add<Value<string>>("", "sampleformat", "Default sample format", settings.sampleFormat, &settings.sampleFormat);
|
||||
op.add<Value<string>>("c", "codec", "Default transport codec\n(flac|ogg|pcm)[:options]\nType codec:? to get codec specific options", settings.codec,
|
||||
&settings.codec);
|
||||
op.add<Value<size_t>>("", "streamBuffer", "Default stream read buffer [ms]", settings.streamReadMs, &settings.streamReadMs);
|
||||
op.add<Value<int>>("b", "buffer", "Buffer [ms]", settings.bufferMs, &settings.bufferMs);
|
||||
op.add<Switch>("", "sendToMuted", "Send audio to muted clients", &settings.sendAudioToMutedClients);
|
||||
#ifdef HAS_DAEMON
|
||||
int processPriority(0);
|
||||
auto daemonOption = op.add<Implicit<int>>("d", "daemon", "Daemonize\noptional process priority [-20..19]", 0, &processPriority);
|
||||
|
@ -95,7 +95,7 @@ int main(int argc, char* argv[])
|
|||
if (versionSwitch->is_set())
|
||||
{
|
||||
cout << "snapserver v" << VERSION << "\n"
|
||||
<< "Copyright (C) 2014-2018 BadAix (snapcast@badaix.de).\n"
|
||||
<< "Copyright (C) 2014-2019 BadAix (snapcast@badaix.de).\n"
|
||||
<< "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n"
|
||||
<< "This is free software: you are free to change and redistribute it.\n"
|
||||
<< "There is NO WARRANTY, to the extent permitted by law.\n\n"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue