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