#include "args.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 "websocket.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef VERSION # define VERSION "unknown" #endif namespace { class Api { public: virtual ~Api() = default; [[nodiscard]] virtual bt::Adapter* adapter() const = 0; protected: Api() = default; }; const std::string_view kSignalUpdateAdapter("controller/update"); class Signaler { public: virtual ~Signaler() = default; virtual void send(std::string_view signal) = 0; virtual std::unique_ptr handle( http::Request const& request) = 0; protected: Signaler() = default; }; std::optional 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 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* 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 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 == "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 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 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("paired"); json_writer_->value(device.paired()); json_writer_->key("connected"); json_writer_->value(device.connected()); json_writer_->end_object(); } Api& api_; Signaler& signaler_; std::unique_ptr json_writer_; std::string json_tmp_; std::unique_ptr json_mimetype_; }; class BluetoothManagerDelegate : public bt::Manager::Delegate, 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 agent_request_pincode( bt::Device& device, std::function)> 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)> 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 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 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 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 handle( http::Request const& request) override { return server_->handle(request); } std::unique_ptr handle(ws::Message const& /* msg */) override { // Ignore anything sent by clients return nullptr; } private: std::unique_ptr server_; }; bool run(logger::Logger& logger, cfg::Config const& cfg, std::unique_ptr port) { auto looper = looper::create(); SignalerImpl signaler(logger, cfg, *looper); BluetoothManagerDelegate bt_delegate(logger, signaler); HttpServerDelegate http_delegate(bt_delegate, signaler); auto server = http::create_server(logger, cfg, *looper, std::move(port), http_delegate); auto manager = bt::create_manager(logger, cfg, *looper, bt_delegate); auto sigint_handler = signals::Handler::create( *looper, signals::Signal::INT, [&looper, &logger]() { logger.info("Received SIGINT, quitting..."); looper->quit(); }); auto sigterm_handler = signals::Handler::create( *looper, signals::Signal::TERM, [&looper, &logger]() { logger.info("Received SIGTERM, quitting..."); looper->quit(); }); return looper->run(logger); } bool start_daemon(logger::Logger& logger, cfg::Config const& cfg, std::unique_ptr port, bool verbose) { auto pid = fork(); if (pid == 0) { // Child process // Move out of current workind directory to not keep it alive chdir("/"); // Create a new process group setpgid(0, 0); // Close stdin, stdout and stderr close(0); close(1); close(2); // Use syslog logger auto logger2 = logger::syslog("bluetooth-jukebox", verbose); _exit(run(*logger2, cfg, std::move(port)) ? 0 : 1); } else if (pid == -1) { // Error logger.err(std::format("Unable to fork: {}", strerror(errno))); return false; } else { // Parent process logger.info(std::format("Forked into pid {}", pid)); return true; } } } // namespace int main(int argc, char** argv) { auto args = Args::create(); auto opt_help = args->option('h', "help", "display this text and exit."); auto opt_version = args->option('V', "version", "display version and exit."); auto opt_config = args->option_argument( 'C', "config", "FILE", "load config from FILE instead of default."); auto opt_verbose = args->option('v', "verbose", "be more verbose."); auto opt_daemon = args->option('d', "daemon", "fork into background."); if (!args->run(argc, argv)) { args->print_error(std::cerr); std::cerr << "Try 'bluetooth-jukebox --help' for more information.\n"; return 1; } if (opt_help->is_set()) { std::cout << "Usage: bluetooth-jukebox [OPTION...]\n" << "\n"; args->print_help(std::cout); return 0; } if (opt_version->is_set()) { std::cout << "bluetooth-jukebox " << VERSION << " written by Joel Klinghed .\n"; return 0; } bool verbose = opt_verbose->is_set(); bool daemon = opt_daemon->is_set(); std::unique_ptr cfg; std::vector errors; if (opt_config->is_set()) { cfg = cfg::load_one(opt_config->argument(), errors); } else { cfg = cfg::load_all("bluetooth-jukebox", errors); } auto logger = logger::stderr(verbose); // Open the port here, in case we fork() later, to fail early. auto port = http::open_port(cfg->get("http.bind").value_or("localhost:5555"), *logger); if (!port) return 1; if (daemon) return start_daemon(*logger, *cfg, std::move(port), verbose) ? 0 : 1; return run(*logger, *cfg, std::move(port)) ? 0 : 1; }