1 /***
2     This file is part of snapcast
3     Copyright (C) 2014-2021  Johannes Pohl
4 
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 ***/
18 
19 #include "control_session_http.hpp"
20 #include "common/aixlog.hpp"
21 #include "control_session_ws.hpp"
22 #include "message/pcm_chunk.hpp"
23 #include "stream_session_ws.hpp"
24 #include <boost/beast/http/file_body.hpp>
25 #include <iostream>
26 
27 using namespace std;
28 
29 static constexpr auto LOG_TAG = "ControlSessionHTTP";
30 
31 
32 static constexpr const char* HTTP_SERVER_NAME = "Snapcast";
33 static constexpr const char* UNCONFIGURED =
34     "<html><head><title>Snapcast Default Page</title></head>"
35     "<body>"
36     "  <h1>Snapcast Default Page</h1>"
37     "  <p>"
38     "    This is the default welcome page used to test the correct operation of the Snapcast built-in webserver."
39     "  </p>"
40     "  <p>"
41     "    This webserver is a websocket endpoint for control clients (ws://<i>host</i>:1780/jsonrpc) and streaming clients"
42     "    (ws://<i>host</i>:1780/stream), but it can also host simple web pages. To serve a web page, you must configure the"
43     "    document root in the snapserver configuration file <b>snapserver.conf</b>, usually located in"
44     "    <b>/etc/snapserver.conf</b>"
45     "  </p>"
46     "  <p>"
47     "    The Snapserver installation should include a copy of <a href=\"https://github.com/badaix/snapweb\">Snapweb</a>,"
48     "    located in <b>/usr/share/snapserver/snapweb/</b><br>"
49     "    To activate it, please configure the <b>doc_root</b> as follows, and restart Snapserver to activate the changes:"
50     "  </p>"
51     "  <pre>"
52     "# HTTP RPC #####################################\n"
53     "#\n"
54     "[http]\n"
55     "\n"
56     "...\n"
57     "\n"
58     "# serve a website from the doc_root location\n"
59     "doc_root = /usr/share/snapserver/snapweb/\n"
60     "\n"
61     "#\n"
62     "################################################</pre>"
63     "</body>"
64     "</html>";
65 
66 namespace
67 {
68 // Return a reasonable mime type based on the extension of a file.
mime_type(boost::beast::string_view path)69 boost::beast::string_view mime_type(boost::beast::string_view path)
70 {
71     using boost::beast::iequals;
72     auto const ext = [&path] {
73         auto const pos = path.rfind(".");
74         if (pos == boost::beast::string_view::npos)
75             return boost::beast::string_view{};
76         return path.substr(pos);
77     }();
78     if (iequals(ext, ".htm"))
79         return "text/html";
80     if (iequals(ext, ".html"))
81         return "text/html";
82     if (iequals(ext, ".php"))
83         return "text/html";
84     if (iequals(ext, ".css"))
85         return "text/css";
86     if (iequals(ext, ".txt"))
87         return "text/plain";
88     if (iequals(ext, ".js"))
89         return "application/javascript";
90     if (iequals(ext, ".json"))
91         return "application/json";
92     if (iequals(ext, ".xml"))
93         return "application/xml";
94     if (iequals(ext, ".swf"))
95         return "application/x-shockwave-flash";
96     if (iequals(ext, ".flv"))
97         return "video/x-flv";
98     if (iequals(ext, ".png"))
99         return "image/png";
100     if (iequals(ext, ".jpe"))
101         return "image/jpeg";
102     if (iequals(ext, ".jpeg"))
103         return "image/jpeg";
104     if (iequals(ext, ".jpg"))
105         return "image/jpeg";
106     if (iequals(ext, ".gif"))
107         return "image/gif";
108     if (iequals(ext, ".bmp"))
109         return "image/bmp";
110     if (iequals(ext, ".ico"))
111         return "image/vnd.microsoft.icon";
112     if (iequals(ext, ".tiff"))
113         return "image/tiff";
114     if (iequals(ext, ".tif"))
115         return "image/tiff";
116     if (iequals(ext, ".svg"))
117         return "image/svg+xml";
118     if (iequals(ext, ".svgz"))
119         return "image/svg+xml";
120     return "application/text";
121 }
122 
123 // Append an HTTP rel-path to a local filesystem path.
124 // The returned path is normalized for the platform.
path_cat(boost::beast::string_view base,boost::beast::string_view path)125 std::string path_cat(boost::beast::string_view base, boost::beast::string_view path)
126 {
127     if (base.empty())
128         return path.to_string();
129     std::string result = base.to_string();
130     char constexpr path_separator = '/';
131     if (result.back() == path_separator)
132         result.resize(result.size() - 1);
133     result.append(path.data(), path.size());
134     return result;
135 }
136 } // namespace
137 
ControlSessionHttp(ControlMessageReceiver * receiver,boost::asio::io_context & ioc,tcp::socket && socket,const ServerSettings::Http & settings)138 ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, tcp::socket&& socket,
139                                        const ServerSettings::Http& settings)
140     : ControlSession(receiver), socket_(std::move(socket)), settings_(settings), strand_(ioc)
141 {
142     LOG(DEBUG, LOG_TAG) << "ControlSessionHttp\n";
143 }
144 
145 
~ControlSessionHttp()146 ControlSessionHttp::~ControlSessionHttp()
147 {
148     LOG(DEBUG, LOG_TAG) << "ControlSessionHttp::~ControlSessionHttp()\n";
149     stop();
150 }
151 
152 
start()153 void ControlSessionHttp::start()
154 {
155     http::async_read(
156         socket_, buffer_, req_,
157         boost::asio::bind_executor(strand_, [this, self = shared_from_this()](boost::system::error_code ec, std::size_t bytes) { on_read(ec, bytes); }));
158 }
159 
160 
161 // This function produces an HTTP response for the given
162 // request. The type of the response object depends on the
163 // contents of the request, so the interface requires the
164 // caller to pass a generic lambda for receiving the response.
165 template <class Body, class Allocator, class Send>
handle_request(http::request<Body,http::basic_fields<Allocator>> && req,Send && send)166 void ControlSessionHttp::handle_request(http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send)
167 {
168     // Returns a bad request response
169     auto const bad_request = [&req](boost::beast::string_view why) {
170         http::response<http::string_body> res{http::status::bad_request, req.version()};
171         // TODO: Server: Snapcast/VERSION
172         res.set(http::field::server, HTTP_SERVER_NAME);
173         res.set(http::field::content_type, "text/html");
174         res.keep_alive(req.keep_alive());
175         res.body() = why.to_string();
176         res.prepare_payload();
177         return res;
178     };
179 
180     // Returns a not found response
181     auto const not_found = [&req](boost::beast::string_view target) {
182         http::response<http::string_body> res{http::status::not_found, req.version()};
183         res.set(http::field::server, HTTP_SERVER_NAME);
184         res.set(http::field::content_type, "text/html");
185         res.keep_alive(req.keep_alive());
186         res.body() = "The resource '" + target.to_string() + "' was not found.";
187         res.prepare_payload();
188         return res;
189     };
190 
191     // Returns a configuration help
192     auto const unconfigured = [&req]() {
193         http::response<http::string_body> res{http::status::ok, req.version()};
194         res.set(http::field::server, HTTP_SERVER_NAME);
195         res.set(http::field::content_type, "text/html");
196         res.keep_alive(req.keep_alive());
197         res.body() = UNCONFIGURED;
198         res.prepare_payload();
199         return res;
200     };
201 
202     // Returns a server error response
203     auto const server_error = [&req](boost::beast::string_view what) {
204         http::response<http::string_body> res{http::status::internal_server_error, req.version()};
205         res.set(http::field::server, HTTP_SERVER_NAME);
206         res.set(http::field::content_type, "text/html");
207         res.keep_alive(req.keep_alive());
208         res.body() = "An error occurred: '" + what.to_string() + "'";
209         res.prepare_payload();
210         return res;
211     };
212 
213     // Make sure we can handle the method
214     if ((req.method() != http::verb::get) && (req.method() != http::verb::head) && (req.method() != http::verb::post))
215         return send(bad_request("Unknown HTTP-method"));
216 
217     // handle json rpc requests
218     if (req.method() == http::verb::post)
219     {
220         if (req.target() != "/jsonrpc")
221             return send(bad_request("Illegal request-target"));
222 
223         string response = message_receiver_->onMessageReceived(this, req.body());
224         http::response<http::string_body> res{http::status::ok, req.version()};
225         res.set(http::field::server, HTTP_SERVER_NAME);
226         res.set(http::field::content_type, "application/json");
227         res.keep_alive(req.keep_alive());
228         res.body() = response;
229         res.prepare_payload();
230         return send(std::move(res));
231     }
232 
233     // Request path must be absolute and not contain "..".
234     if (req.target().empty() || req.target()[0] != '/' || req.target().find("..") != beast::string_view::npos)
235         return send(bad_request("Illegal request-target"));
236 
237     // Build the path to the requested file
238     std::string path = path_cat(settings_.doc_root, req.target());
239     if (req.target().back() == '/')
240         path.append("index.html");
241 
242     if (settings_.doc_root.empty())
243     {
244         std::string default_page = "/usr/share/snapserver/index.html";
245         struct stat buffer;
246         if (stat(default_page.c_str(), &buffer) == 0)
247             path = default_page;
248         else
249             return send(unconfigured());
250     }
251 
252     LOG(DEBUG, LOG_TAG) << "path: " << path << "\n";
253     // Attempt to open the file
254     beast::error_code ec;
255     http::file_body::value_type body;
256     body.open(path.c_str(), beast::file_mode::scan, ec);
257 
258     // Handle the case where the file doesn't exist
259     if (ec == boost::system::errc::no_such_file_or_directory)
260         return send(not_found(req.target()));
261 
262     // Handle an unknown error
263     if (ec)
264         return send(server_error(ec.message()));
265 
266     // Cache the size since we need it after the move
267     auto const size = body.size();
268 
269     // Respond to HEAD request
270     if (req.method() == http::verb::head)
271     {
272         http::response<http::empty_body> res{http::status::ok, req.version()};
273         res.set(http::field::server, HTTP_SERVER_NAME);
274         res.set(http::field::content_type, mime_type(path));
275         res.content_length(size);
276         res.keep_alive(req.keep_alive());
277         return send(std::move(res));
278     }
279 
280     // Respond to GET request
281     http::response<http::file_body> res{std::piecewise_construct, std::make_tuple(std::move(body)), std::make_tuple(http::status::ok, req.version())};
282     res.set(http::field::server, HTTP_SERVER_NAME);
283     res.set(http::field::content_type, mime_type(path));
284     res.content_length(size);
285     res.keep_alive(req.keep_alive());
286     return send(std::move(res));
287 }
288 
on_read(beast::error_code ec,std::size_t bytes_transferred)289 void ControlSessionHttp::on_read(beast::error_code ec, std::size_t bytes_transferred)
290 {
291     // This means they closed the connection
292     if (ec == http::error::end_of_stream)
293     {
294         socket_.shutdown(tcp::socket::shutdown_send, ec);
295         return;
296     }
297 
298     // Handle the error, if any
299     if (ec)
300     {
301         LOG(ERROR, LOG_TAG) << "ControlSessionHttp::on_read error: " << ec.message() << "\n";
302         return;
303     }
304 
305     LOG(DEBUG, LOG_TAG) << "read: " << bytes_transferred << ", method: " << req_.method_string() << ", content type: " << req_[beast::http::field::content_type]
306                         << ", target: " << req_.target() << ", body: " << req_.body() << "\n";
307 
308     // See if it is a WebSocket Upgrade
309     if (websocket::is_upgrade(req_))
310     {
311         LOG(DEBUG, LOG_TAG) << "websocket upgrade, target: " << req_.target() << "\n";
312         if (req_.target() == "/jsonrpc")
313         {
314             // Create a WebSocket session by transferring the socket
315             // std::make_shared<websocket_session>(std::move(socket_), state_)->run(std::move(req_));
316             auto ws = std::make_shared<websocket::stream<beast::tcp_stream>>(std::move(socket_));
317             ws->async_accept(req_, [this, ws, self = shared_from_this()](beast::error_code ec) {
318                 if (ec)
319                 {
320                     LOG(ERROR, LOG_TAG) << "Error during WebSocket handshake (control): " << ec.message() << "\n";
321                 }
322                 else
323                 {
324                     auto ws_session = make_shared<ControlSessionWebsocket>(message_receiver_, strand_.context(), std::move(*ws));
325                     message_receiver_->onNewSession(ws_session);
326                 }
327             });
328         }
329         else if (req_.target() == "/stream")
330         {
331             // Create a WebSocket session by transferring the socket
332             // std::make_shared<websocket_session>(std::move(socket_), state_)->run(std::move(req_));
333             auto ws = std::make_shared<websocket::stream<beast::tcp_stream>>(std::move(socket_));
334             ws->async_accept(req_, [this, ws, self = shared_from_this()](beast::error_code ec) {
335                 if (ec)
336                 {
337                     LOG(ERROR, LOG_TAG) << "Error during WebSocket handshake (stream): " << ec.message() << "\n";
338                 }
339                 else
340                 {
341                     auto ws_session = make_shared<StreamSessionWebsocket>(strand_.context(), nullptr, std::move(*ws));
342                     message_receiver_->onNewSession(ws_session);
343                 }
344             });
345         }
346         return;
347     }
348 
349     // Send the response
350     handle_request(std::move(req_), [this](auto&& response) {
351         // The lifetime of the message has to extend
352         // for the duration of the async operation so
353         // we use a shared_ptr to manage it.
354         using response_type = typename std::decay<decltype(response)>::type;
355         auto sp = std::make_shared<response_type>(std::forward<decltype(response)>(response));
356 
357         // Write the response
358         http::async_write(this->socket_, *sp,
359                           boost::asio::bind_executor(strand_, [this, self = this->shared_from_this(), sp](beast::error_code ec, std::size_t bytes) {
360                               this->on_write(ec, bytes, sp->need_eof());
361                           }));
362     });
363 }
364 
365 
on_write(beast::error_code ec,std::size_t bytes,bool close)366 void ControlSessionHttp::on_write(beast::error_code ec, std::size_t bytes, bool close)
367 {
368     std::ignore = bytes;
369 
370     // Handle the error, if any
371     if (ec)
372     {
373         LOG(ERROR, LOG_TAG) << "ControlSessionHttp::on_write, error: " << ec.message() << "\n";
374         return;
375     }
376 
377     if (close)
378     {
379         // This means we should close the connection, usually because
380         // the response indicated the "Connection: close" semantic.
381         socket_.shutdown(tcp::socket::shutdown_send, ec);
382         return;
383     }
384 
385     // Clear contents of the request message,
386     // otherwise the read behavior is undefined.
387     req_ = {};
388 
389     // Read another request
390     http::async_read(socket_, buffer_, req_,
391                      boost::asio::bind_executor(strand_, [this, self = shared_from_this()](beast::error_code ec, std::size_t bytes) { on_read(ec, bytes); }));
392 }
393 
394 
stop()395 void ControlSessionHttp::stop()
396 {
397 }
398 
399 
sendAsync(const std::string &)400 void ControlSessionHttp::sendAsync(const std::string& /*message*/)
401 {
402 }
403