summaryrefslogtreecommitdiff
path: root/src/server.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/server.cc')
-rw-r--r--src/server.cc397
1 files changed, 397 insertions, 0 deletions
diff --git a/src/server.cc b/src/server.cc
new file mode 100644
index 0000000..9b556b0
--- /dev/null
+++ b/src/server.cc
@@ -0,0 +1,397 @@
+#include "server.hh"
+
+#include "bt.hh"
+#include "cfg.hh"
+#include "config.h"
+#include "http.hh"
+#include "json.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include "signals.hh"
+#include "uri.hh"
+#include "websocket.hh"
+
+#include <cstdint>
+#include <format>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+
+namespace server {
+
+namespace {
+
+const std::string_view kSignalUpdateAdapter("controller/update");
+const std::string kSignalUpdateDevicePrefix("device/update/");
+
+std::optional<bool> match_bool(std::string_view str) {
+ if (str == "true")
+ return true;
+ if (str == "false")
+ return false;
+ return std::nullopt;
+}
+
+class HttpServerDelegate : public http::Server::Delegate {
+ public:
+ HttpServerDelegate(Api& api, Signaler& signaler)
+ : api_(api),
+ signaler_(signaler),
+ json_writer_(json::writer(json_tmp_)),
+ json_mimetype_(http::MimeType::create("application", "json")) {}
+
+ std::unique_ptr<http::Response> handle(
+ http::Request const& request) override {
+ if (request.path().starts_with("/api/v1/")) {
+ auto path = request.path().substr(8);
+ if (path == "status") {
+ if (request.method() != "GET") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+
+ return status_ok();
+ }
+
+ if (path == "controller") {
+ if (request.method() != "GET") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+
+ auto const* adapter = api_.adapter();
+ json_writer_->clear();
+ write_adapter(adapter);
+
+ return http::Response::content(json_tmp_, *json_mimetype_);
+ }
+
+ if (path == "controller/discoverable") {
+ if (request.method() != "POST") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+ std::optional<bool> value = match_bool(request.body());
+ auto* adapter = api_.adapter();
+ if (adapter && value.has_value()) {
+ adapter->set_discoverable(value.value());
+ return status_ok();
+ }
+ return status_error("Bad state");
+ }
+
+ if (path.starts_with("device/")) {
+ if (request.method() != "GET") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+
+ std::string tmp;
+ auto address = uri::decode(path.substr(7), tmp);
+ auto const* adapter = api_.adapter();
+ if (adapter) {
+ for (auto* device : adapter->devices()) {
+ if (device->address() == address) {
+ json_writer_->clear();
+ write_device(*device);
+ return http::Response::content(json_tmp_, *json_mimetype_);
+ }
+ }
+ }
+ return http::Response::status(http::StatusCode::kNotFound);
+ }
+
+ if (path == "events") {
+ if (request.method() != "GET") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+
+ auto resp = signaler_.handle(request);
+ if (resp)
+ return resp;
+ return http::Response::status(http::StatusCode::kBadRequest);
+ }
+ }
+
+ return http::Response::status(http::StatusCode::kNotFound);
+ }
+
+ private:
+ std::unique_ptr<http::Response> status_ok() {
+ json_writer_->clear();
+ json_writer_->start_object();
+ json_writer_->key("status");
+ json_writer_->value("OK");
+ json_writer_->end_object();
+ return http::Response::content(json_tmp_, *json_mimetype_);
+ }
+
+ std::unique_ptr<http::Response> status_error(std::string_view message) {
+ json_writer_->clear();
+ json_writer_->start_object();
+ json_writer_->key("status");
+ json_writer_->value("error");
+ json_writer_->key("message");
+ json_writer_->value(message);
+ json_writer_->end_object();
+ return http::Response::content(json_tmp_, *json_mimetype_);
+ }
+
+ void write_adapter(bt::Adapter const* adapter) {
+ json_writer_->start_object();
+ json_writer_->key("name");
+ json_writer_->value(adapter ? adapter->name() : "unknown");
+ json_writer_->key("pairable");
+ json_writer_->value(adapter ? adapter->pairable() : false);
+ json_writer_->key("discoverable");
+ json_writer_->value(adapter ? adapter->discoverable() : false);
+ json_writer_->key("discoverable_timeout_seconds");
+ json_writer_->value(adapter ? adapter->discoverable_timeout_seconds() : 0);
+ json_writer_->key("pairing");
+ json_writer_->value(adapter ? adapter->pairing() : false);
+
+ json_writer_->key("devices");
+ json_writer_->start_array();
+
+ if (adapter) {
+ for (auto* device : adapter->devices()) {
+ write_device(*device);
+ }
+ }
+
+ json_writer_->end_array();
+ json_writer_->end_object();
+ }
+
+ void write_device(bt::Device const& device) {
+ json_writer_->start_object();
+ json_writer_->key("name");
+ json_writer_->value(device.name());
+ json_writer_->key("address");
+ json_writer_->value(device.address());
+ json_writer_->key("paired");
+ json_writer_->value(device.paired());
+ json_writer_->key("connected");
+ json_writer_->value(device.connected());
+
+ json_writer_->key("playing");
+ if (auto* player = device.player()) {
+ json_writer_->start_object();
+ json_writer_->key("status");
+ switch (player->status()) {
+ case bt::Player::Status::kPlaying:
+ case bt::Player::Status::kForwardSeek:
+ case bt::Player::Status::kReverseSeek:
+ json_writer_->value("playing");
+ break;
+ case bt::Player::Status::kStopped:
+ json_writer_->value("stopped");
+ break;
+ case bt::Player::Status::kPaused:
+ json_writer_->value("paused");
+ break;
+ case bt::Player::Status::kError:
+ json_writer_->value("error");
+ break;
+ }
+ json_writer_->key("title");
+ json_writer_->value(player->track_title());
+ json_writer_->key("album");
+ json_writer_->value(player->track_album());
+ json_writer_->key("artist");
+ json_writer_->value(player->track_artist());
+ json_writer_->end_object();
+ } else {
+ json_writer_->value(nullptr);
+ }
+
+ json_writer_->end_object();
+ }
+
+ Api& api_;
+ Signaler& signaler_;
+ std::unique_ptr<json::Writer> json_writer_;
+ std::string json_tmp_;
+ std::unique_ptr<http::MimeType> json_mimetype_;
+};
+
+class BluetoothManagerDelegate : public Api {
+ public:
+ BluetoothManagerDelegate(logger::Logger& logger, Signaler& signaler)
+ : logger_(logger), signaler_(signaler) {}
+
+ [[nodiscard]]
+ bt::Adapter* adapter() const override {
+ return adapter_;
+ }
+
+ void new_adapter(bt::Adapter* adapter) override {
+ adapter_ = adapter;
+
+ signaler_.send(kSignalUpdateAdapter);
+
+ if (adapter) {
+ logger_.info(std::format("New adapter: {} [{}]", adapter->name(),
+ adapter->address()));
+
+ // Assuming pairable doesn't have a timeout - make it so.
+ if (adapter_->pairable_timeout_seconds() > 0) {
+ adapter_->set_pairable_timeout_seconds(0);
+ }
+
+ // Assuming discoverable has one
+ if (adapter_->discoverable_timeout_seconds() == 0) {
+ adapter_->set_discoverable_timeout_seconds(180);
+ }
+ } else {
+ logger_.info("No adapter");
+ }
+ }
+
+ void updated_adapter(bt::Adapter& adapter) override {
+ if (adapter_ == &adapter)
+ signaler_.send(kSignalUpdateAdapter);
+ }
+
+ void added_device(bt::Device& device) override {
+ logger_.info(
+ std::format("New device: {} [{}]", device.name(), device.address()));
+
+ if (adapter_)
+ signaler_.send(kSignalUpdateAdapter);
+ }
+
+ void removed_device(std::string const& address) override {
+ logger_.info(std::format("Remove device: [{}]", address));
+
+ if (adapter_)
+ signaler_.send(kSignalUpdateAdapter);
+ }
+
+ void added_player(bt::Device& device, bt::Player& /* player */) override {
+ logger_.info(std::format("New player for {}", device.name()));
+
+ if (adapter_)
+ signaler_.send(kSignalUpdateDevicePrefix + device.address());
+ }
+
+ void removed_player(bt::Device& device) override {
+ logger_.info(std::format("Remove player for {}", device.name()));
+
+ if (adapter_)
+ signaler_.send(kSignalUpdateDevicePrefix + device.address());
+ }
+
+ void updated_player(bt::Device& device, bt::Player& /* player */) override {
+ if (adapter_)
+ signaler_.send(kSignalUpdateDevicePrefix + device.address());
+ }
+
+ void agent_request_pincode(
+ bt::Device& device,
+ std::function<void(std::optional<std::string>)> callback) override {
+ logger_.dbg(std::format("Device request pincode: {}", device.name()));
+ callback(std::nullopt);
+ }
+
+ void agent_display_pincode(bt::Device& device,
+ std::string const& pincode) override {
+ logger_.dbg(std::format("Device pincode: {} {}", device.name(), pincode));
+ }
+
+ void agent_request_passkey(
+ bt::Device& device,
+ std::function<void(std::optional<uint32_t>)> callback) override {
+ logger_.dbg(std::format("Device request passkey: {}", device.name()));
+ callback(std::nullopt);
+ }
+
+ void agent_display_passkey(bt::Device& device, uint32_t passkey,
+ uint16_t /* entered */) override {
+ logger_.dbg(std::format("Device passkey: {} {}", device.name(), passkey));
+ }
+
+ void agent_request_confirmation(bt::Device& device, uint32_t passkey,
+ std::function<void(bool)> callback) override {
+ logger_.dbg(std::format("Device request confirmation: {} {}", device.name(),
+ passkey));
+ // Confirm all
+ callback(true);
+ }
+
+ void agent_request_authorization(
+ bt::Device& device, std::function<void(bool)> callback) override {
+ logger_.dbg(std::format("Device request authorization: {}", device.name()));
+ callback(false);
+ }
+
+ void agent_authorize_service(bt::Device& device, std::string const& uuid,
+ std::function<void(bool)> callback) override {
+ if (uuid == "0000110d-0000-1000-8000-00805f9b34fb") {
+ // Advanced Audio Distribution Profile
+ callback(true);
+ return;
+ }
+ if (uuid == "0000111e-0000-1000-8000-00805f9b34fb") {
+ // Hands-Free Service Class and Profile
+ // Not interrested.
+ callback(false);
+ return;
+ }
+
+ logger_.dbg(std::format("Device request authorize unknown service: {} {}",
+ device.name(), uuid));
+
+ // Do not authorize unknown services.
+ callback(false);
+ }
+
+ void agent_cancel() override {}
+
+ private:
+ logger::Logger& logger_;
+ Signaler& signaler_;
+ bt::Adapter* adapter_{nullptr};
+};
+
+class SignalerImpl : public Signaler, ws::Server::Delegate {
+ public:
+ SignalerImpl(logger::Logger& logger, cfg::Config const& cfg,
+ looper::Looper& looper)
+ : server_(ws::create_server(logger, cfg, looper, *this)) {}
+
+ void send(std::string_view signal) override {
+ server_->send_text_to_all(signal);
+ }
+
+ std::unique_ptr<http::Response> handle(
+ http::Request const& request) override {
+ return server_->handle(request);
+ }
+
+ std::unique_ptr<ws::Message> handle(ws::Message const& /* msg */) override {
+ // Ignore anything sent by clients
+ return nullptr;
+ }
+
+ private:
+ std::unique_ptr<ws::Server> server_;
+};
+
+} // namespace
+
+std::unique_ptr<Signaler> create_signaler(logger::Logger& logger,
+ cfg::Config const& cfg,
+ looper::Looper& looper) {
+ return std::make_unique<SignalerImpl>(logger, cfg, looper);
+}
+
+std::unique_ptr<Api> create_bt_delegate(logger::Logger& logger,
+ Signaler& signaler) {
+ return std::make_unique<BluetoothManagerDelegate>(logger, signaler);
+}
+
+std::unique_ptr<http::Server::Delegate> create_http_delegate(
+ Api& api, Signaler& signaler) {
+ return std::make_unique<HttpServerDelegate>(api, signaler);
+}
+
+} // namespace server