1006 lines
24 KiB
C
1006 lines
24 KiB
C
/*
|
|
* Copyright (c) 2025 Amlogic, Inc. All rights reserved.
|
|
*
|
|
* This source code is subject to the terms and conditions defined in the
|
|
* file 'LICENSE' which is part of this source code package.
|
|
*
|
|
* default_agent.c - Standalone BlueZ agent daemon
|
|
*
|
|
* Monitors org.bluez on the system D-Bus, registers itself as the default
|
|
* agent (org.bluez.Agent1) and auto-confirms pairing requests.
|
|
*
|
|
* This agent is intended for embedded unattended devices (e.g. set-top
|
|
* boxes) where there is no interactive user: pairing confirmations are
|
|
* automatically accepted. Only enable it on trusted products via
|
|
* USE_DEFAULT_AGENT=y.
|
|
*
|
|
* Passkey handling:
|
|
* - default passkey is 0000
|
|
* - command line option -p <passkey> sets a fixed passkey
|
|
* - at runtime, typing "passkey <digits>" on stdin (e.g. serial console)
|
|
* updates the passkey; "passkey 000000" restores the default 0000
|
|
*
|
|
* Build: see Makefile (USE_DEFAULT_AGENT=y)
|
|
* Depends only on gio-2.0/glib-2.0/dbus-1.
|
|
*/
|
|
#include <gio/gio.h>
|
|
#include <glib-unix.h>
|
|
|
|
#include <ctype.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define AGENT_PATH "/org/bluez/agent"
|
|
#define AGENT_INTERFACE "org.bluez.Agent1"
|
|
|
|
#define BLUEZ_NAME "org.bluez"
|
|
#define AGENT_MANAGER_PATH "/org/bluez"
|
|
#define AGENT_MANAGER_IFACE "org.bluez.AgentManager1"
|
|
|
|
#define DBUS_NAME "org.freedesktop.DBus"
|
|
#define DBUS_PATH "/org/freedesktop/DBus"
|
|
#define DBUS_IFACE "org.freedesktop.DBus"
|
|
|
|
#define DEFAULT_PASSKEY "0000"
|
|
#define DEFAULT_CAPABILITY "NoInputNoOutput"
|
|
|
|
#define AGENT_PROMPT "default-agent: "
|
|
|
|
#define PASSKEY_MAX_LEN 6
|
|
|
|
static const gchar *opt_capability = NULL;
|
|
static gchar *opt_passkey = NULL;
|
|
static gboolean opt_debug = FALSE;
|
|
|
|
static gchar passkey[PASSKEY_MAX_LEN + 1];
|
|
|
|
static GDBusConnection *dbus_conn = NULL;
|
|
|
|
static gboolean agent_registered = FALSE;
|
|
static guint agent_reg_id = 0;
|
|
static guint name_owner_sub_id = 0;
|
|
static guint stdin_watch_id = 0;
|
|
|
|
static GMainLoop *main_loop = NULL;
|
|
|
|
static const GDBusInterfaceVTable agent_vtable;
|
|
static const GDBusInterfaceInfo agent_interface_info;
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Logging */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
#define info(fmt, ...) g_print(AGENT_PROMPT fmt "\n", ##__VA_ARGS__)
|
|
|
|
#define err(fmt, ...) g_printerr(AGENT_PROMPT "ERROR: " fmt "\n", ##__VA_ARGS__)
|
|
|
|
#define dbg(fmt, ...) \
|
|
G_STMT_START { \
|
|
if (opt_debug) \
|
|
g_print(AGENT_PROMPT "DEBUG: " fmt "\n", ##__VA_ARGS__); \
|
|
} \
|
|
G_STMT_END
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Passkey */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static gboolean validate_passkey(const gchar *str) {
|
|
gsize len;
|
|
|
|
if (!str)
|
|
return FALSE;
|
|
|
|
len = strlen(str);
|
|
|
|
if (len == 0 || len > PASSKEY_MAX_LEN)
|
|
return FALSE;
|
|
|
|
if (strspn(str, "0123456789") != len)
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean set_passkey_internal(const gchar *str, gboolean verbose) {
|
|
gsize len;
|
|
|
|
if (!validate_passkey(str)) {
|
|
if (verbose) {
|
|
info("invalid passkey '%s', use 1-%d digits", str ? str : "(null)",
|
|
PASSKEY_MAX_LEN);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
len = strlen(str);
|
|
|
|
memset(passkey, 0, sizeof(passkey));
|
|
memcpy(passkey, str, len);
|
|
|
|
if (verbose)
|
|
info("passkey set to %s", passkey);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void set_passkey_from_command(const gchar *str) {
|
|
gchar *tmp;
|
|
|
|
if (!str) {
|
|
info("usage: passkey <1-%d digits>", PASSKEY_MAX_LEN);
|
|
return;
|
|
}
|
|
|
|
tmp = g_strdup(str);
|
|
g_strstrip(tmp);
|
|
|
|
if (!*tmp) {
|
|
info("usage: passkey <1-%d digits>", PASSKEY_MAX_LEN);
|
|
g_free(tmp);
|
|
return;
|
|
}
|
|
|
|
set_passkey_internal(tmp, TRUE);
|
|
|
|
g_free(tmp);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* BlueZ Agent registration */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static gboolean unregister_agent(void) {
|
|
GVariant *res;
|
|
GError *error = NULL;
|
|
|
|
if (!agent_registered || !dbus_conn)
|
|
return TRUE;
|
|
|
|
info("unregistering agent...");
|
|
|
|
res = g_dbus_connection_call_sync(dbus_conn, BLUEZ_NAME, AGENT_MANAGER_PATH,
|
|
AGENT_MANAGER_IFACE, "UnregisterAgent",
|
|
g_variant_new("(o)", AGENT_PATH), NULL,
|
|
G_DBUS_CALL_FLAGS_NONE, 3000, NULL, &error);
|
|
|
|
if (!res) {
|
|
|
|
/*
|
|
* BlueZ may already have disappeared.
|
|
* In that case the registration is already gone.
|
|
*/
|
|
if (error) {
|
|
dbg("UnregisterAgent failed: %s", error->message);
|
|
|
|
g_error_free(error);
|
|
}
|
|
|
|
} else {
|
|
g_variant_unref(res);
|
|
}
|
|
|
|
agent_registered = FALSE;
|
|
|
|
info("agent unregistered");
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean register_agent(void) {
|
|
GVariant *res;
|
|
GError *error = NULL;
|
|
|
|
if (!dbus_conn)
|
|
return FALSE;
|
|
|
|
if (agent_registered) {
|
|
dbg("agent already registered");
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* Make sure BlueZ is currently available.
|
|
*/
|
|
res = g_dbus_connection_call_sync(
|
|
dbus_conn, DBUS_NAME, DBUS_PATH, DBUS_IFACE, "GetNameOwner",
|
|
g_variant_new("(s)", BLUEZ_NAME), G_VARIANT_TYPE("(s)"),
|
|
G_DBUS_CALL_FLAGS_NONE, 3000, NULL, &error);
|
|
|
|
if (!res) {
|
|
dbg("BlueZ is not available");
|
|
|
|
if (error)
|
|
g_error_free(error);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
g_variant_unref(res);
|
|
|
|
info("registering agent...");
|
|
|
|
/*
|
|
* RegisterAgent(object, capability)
|
|
*/
|
|
res = g_dbus_connection_call_sync(
|
|
dbus_conn, BLUEZ_NAME, AGENT_MANAGER_PATH, AGENT_MANAGER_IFACE,
|
|
"RegisterAgent", g_variant_new("(os)", AGENT_PATH, opt_capability), NULL,
|
|
G_DBUS_CALL_FLAGS_NONE, 3000, NULL, &error);
|
|
|
|
if (!res) {
|
|
|
|
if (error) {
|
|
err("RegisterAgent failed: %s", error->message);
|
|
|
|
g_error_free(error);
|
|
} else {
|
|
err("RegisterAgent failed");
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
g_variant_unref(res);
|
|
|
|
/*
|
|
* RegisterAgent succeeded.
|
|
*/
|
|
agent_registered = TRUE;
|
|
|
|
/*
|
|
* RequestDefaultAgent(object)
|
|
*/
|
|
res = g_dbus_connection_call_sync(dbus_conn, BLUEZ_NAME, AGENT_MANAGER_PATH,
|
|
AGENT_MANAGER_IFACE, "RequestDefaultAgent",
|
|
g_variant_new("(o)", AGENT_PATH), NULL,
|
|
G_DBUS_CALL_FLAGS_NONE, 3000, NULL, &error);
|
|
|
|
if (!res) {
|
|
|
|
if (error) {
|
|
err("RequestDefaultAgent failed: %s", error->message);
|
|
|
|
g_error_free(error);
|
|
} else {
|
|
err("RequestDefaultAgent failed");
|
|
}
|
|
|
|
/*
|
|
* Roll back RegisterAgent.
|
|
*/
|
|
unregister_agent();
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
g_variant_unref(res);
|
|
|
|
info("agent registered as default "
|
|
"(capability=%s, passkey=%s)",
|
|
opt_capability, passkey);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* BlueZ NameOwnerChanged */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void on_name_owner_changed(GDBusConnection *conn,
|
|
const gchar *sender_name,
|
|
const gchar *object_path,
|
|
const gchar *interface_name,
|
|
const gchar *signal_name,
|
|
GVariant *parameters, gpointer user_data) {
|
|
const gchar *name;
|
|
const gchar *old_owner;
|
|
const gchar *new_owner;
|
|
|
|
(void)conn;
|
|
(void)sender_name;
|
|
(void)object_path;
|
|
(void)interface_name;
|
|
(void)signal_name;
|
|
(void)user_data;
|
|
|
|
g_variant_get(parameters, "(&s&s&s)", &name, &old_owner, &new_owner);
|
|
|
|
if (g_strcmp0(name, BLUEZ_NAME) != 0)
|
|
return;
|
|
|
|
dbg("NameOwnerChanged %s: '%s' -> '%s'", name, old_owner, new_owner);
|
|
|
|
/*
|
|
* BlueZ disappeared.
|
|
*/
|
|
if (!new_owner || *new_owner == '\0') {
|
|
|
|
if (agent_registered) {
|
|
agent_registered = FALSE;
|
|
|
|
info("bluez disappeared, agent registration lost");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* BlueZ appeared.
|
|
*/
|
|
if (!old_owner || *old_owner == '\0') {
|
|
|
|
info("bluez appeared, registering agent...");
|
|
|
|
register_agent();
|
|
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* BlueZ owner changed.
|
|
*
|
|
* Usually means bluetoothd restarted.
|
|
*/
|
|
if (g_strcmp0(old_owner, new_owner) != 0) {
|
|
|
|
agent_registered = FALSE;
|
|
|
|
info("bluez owner changed, registering agent...");
|
|
|
|
register_agent();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* D-Bus setup */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static gboolean setup_dbus(void) {
|
|
GError *error = NULL;
|
|
|
|
/*
|
|
* Connect directly to system bus.
|
|
*
|
|
* We intentionally do NOT use g_bus_own_name().
|
|
*/
|
|
dbus_conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
|
|
|
|
if (!dbus_conn) {
|
|
|
|
err("failed to connect to system bus: %s",
|
|
error ? error->message : "unknown error");
|
|
|
|
if (error)
|
|
g_error_free(error);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
info("connected to system bus");
|
|
|
|
/*
|
|
* Export Agent1 object.
|
|
*/
|
|
agent_reg_id = g_dbus_connection_register_object(
|
|
dbus_conn, AGENT_PATH, (GDBusInterfaceInfo *)&agent_interface_info,
|
|
(GDBusInterfaceVTable *)&agent_vtable, NULL, NULL, &error);
|
|
|
|
if (agent_reg_id == 0) {
|
|
|
|
err("failed to register agent object: %s",
|
|
error ? error->message : "unknown error");
|
|
|
|
if (error)
|
|
g_error_free(error);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
info("agent object exported at %s", AGENT_PATH);
|
|
|
|
/*
|
|
* Watch BlueZ NameOwnerChanged.
|
|
*
|
|
* This allows us to handle:
|
|
*
|
|
* 1. BlueZ already running
|
|
* 2. BlueZ starts later
|
|
* 3. BlueZ restarts
|
|
*/
|
|
name_owner_sub_id = g_dbus_connection_signal_subscribe(
|
|
dbus_conn, DBUS_NAME, DBUS_IFACE, "NameOwnerChanged", DBUS_PATH,
|
|
BLUEZ_NAME, G_DBUS_SIGNAL_FLAGS_NONE, on_name_owner_changed, NULL, NULL);
|
|
|
|
if (name_owner_sub_id == 0) {
|
|
|
|
err("failed to subscribe NameOwnerChanged");
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/*
|
|
* Check whether BlueZ is already running.
|
|
*/
|
|
if (!register_agent()) {
|
|
|
|
info("bluez is not running yet, waiting for BlueZ...");
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Agent methods */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void agent_release(void) {
|
|
agent_registered = FALSE;
|
|
|
|
info("agent released by bluez");
|
|
}
|
|
|
|
static void handle_request_pincode(GDBusMethodInvocation *invocation) {
|
|
info("RequestPinCode -> %s", passkey);
|
|
|
|
g_dbus_method_invocation_return_value(invocation,
|
|
g_variant_new("(s)", passkey));
|
|
}
|
|
|
|
static void handle_request_passkey(GDBusMethodInvocation *invocation) {
|
|
guint32 value = 0;
|
|
|
|
value = (guint32)g_ascii_strtoull(passkey, NULL, 10);
|
|
|
|
info("RequestPasskey -> %06u", value);
|
|
|
|
g_dbus_method_invocation_return_value(invocation,
|
|
g_variant_new("(u)", value));
|
|
}
|
|
|
|
static void agent_method_call(GDBusConnection *conn, const gchar *sender,
|
|
const gchar *object_path,
|
|
const gchar *interface_name,
|
|
const gchar *method_name, GVariant *parameters,
|
|
GDBusMethodInvocation *invocation,
|
|
gpointer user_data) {
|
|
(void)conn;
|
|
(void)sender;
|
|
(void)object_path;
|
|
(void)interface_name;
|
|
(void)user_data;
|
|
|
|
if (g_strcmp0(method_name, "Release") == 0) {
|
|
|
|
agent_release();
|
|
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "RequestPinCode") == 0) {
|
|
|
|
handle_request_pincode(invocation);
|
|
|
|
} else if (g_strcmp0(method_name, "RequestPasskey") == 0) {
|
|
|
|
handle_request_passkey(invocation);
|
|
|
|
} else if (g_strcmp0(method_name, "DisplayPinCode") == 0) {
|
|
|
|
const gchar *device;
|
|
const gchar *pin;
|
|
|
|
g_variant_get(parameters, "(&o&s)", &device, &pin);
|
|
|
|
info("DisplayPinCode device=%s pin=%s", device, pin);
|
|
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "DisplayPasskey") == 0) {
|
|
|
|
const gchar *device;
|
|
guint32 passkey_val;
|
|
guint16 entered;
|
|
|
|
g_variant_get(parameters, "(&ouq)", &device, &passkey_val, &entered);
|
|
|
|
info("DisplayPasskey device=%s passkey=%06u entered=%u", device,
|
|
passkey_val, entered);
|
|
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "RequestConfirmation") == 0) {
|
|
|
|
const gchar *device;
|
|
guint32 passkey_val;
|
|
|
|
g_variant_get(parameters, "(&ou)", &device, &passkey_val);
|
|
|
|
/*
|
|
* Embedded unattended device:
|
|
* automatically accept confirmation.
|
|
*/
|
|
info("RequestConfirmation device=%s passkey=%06u -> auto accept", device,
|
|
passkey_val);
|
|
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "RequestAuthorization") == 0) {
|
|
|
|
const gchar *device;
|
|
|
|
g_variant_get(parameters, "(&o)", &device);
|
|
|
|
info("RequestAuthorization device=%s -> auto accept", device);
|
|
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "AuthorizeService") == 0) {
|
|
|
|
const gchar *device;
|
|
const gchar *uuid;
|
|
|
|
g_variant_get(parameters, "(&o&s)", &device, &uuid);
|
|
|
|
info("AuthorizeService device=%s uuid=%s -> auto accept", device, uuid);
|
|
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "Cancel") == 0) {
|
|
|
|
info("Cancel");
|
|
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else {
|
|
|
|
err("unknown Agent1 method: %s", method_name);
|
|
|
|
g_dbus_method_invocation_return_dbus_error(
|
|
invocation, "org.bluez.Error.InvalidArgs", "Unknown method");
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Agent interface description */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static const GDBusInterfaceVTable agent_vtable = {
|
|
.method_call = agent_method_call,
|
|
.get_property = NULL,
|
|
.set_property = NULL,
|
|
};
|
|
|
|
/*
|
|
* Common arguments.
|
|
*/
|
|
|
|
static const GDBusArgInfo arg_device = {-1, "device", "o", NULL};
|
|
|
|
static const GDBusArgInfo arg_pin = {-1, "pincode", "s", NULL};
|
|
|
|
static const GDBusArgInfo arg_passkey = {-1, "passkey", "u", NULL};
|
|
|
|
static const GDBusArgInfo arg_entered = {-1, "entered", "q", NULL};
|
|
|
|
static const GDBusArgInfo arg_uuid = {-1, "uuid", "s", NULL};
|
|
|
|
static const GDBusArgInfo arg_out_pin = {-1, "pincode", "s", NULL};
|
|
|
|
static const GDBusArgInfo arg_out_passkey = {-1, "passkey", "u", NULL};
|
|
|
|
/*
|
|
* RequestPinCode(o) -> (s)
|
|
*/
|
|
|
|
static const GDBusArgInfo *req_pin_in[] = {&arg_device, NULL};
|
|
|
|
static const GDBusArgInfo *req_pin_out[] = {&arg_out_pin, NULL};
|
|
|
|
/*
|
|
* RequestPasskey(o) -> (u)
|
|
*/
|
|
|
|
static const GDBusArgInfo *req_passkey_in[] = {&arg_device, NULL};
|
|
|
|
static const GDBusArgInfo *req_passkey_out[] = {&arg_out_passkey, NULL};
|
|
|
|
/*
|
|
* DisplayPinCode(o, s)
|
|
*/
|
|
|
|
static const GDBusArgInfo *display_pin_in[] = {&arg_device, &arg_pin, NULL};
|
|
|
|
/*
|
|
* DisplayPasskey(o, u, q)
|
|
*/
|
|
|
|
static const GDBusArgInfo *display_passkey_in[] = {&arg_device, &arg_passkey,
|
|
&arg_entered, NULL};
|
|
|
|
/*
|
|
* RequestConfirmation(o, u)
|
|
*/
|
|
|
|
static const GDBusArgInfo *confirm_in[] = {&arg_device, &arg_passkey, NULL};
|
|
|
|
/*
|
|
* RequestAuthorization(o)
|
|
*/
|
|
|
|
static const GDBusArgInfo *authorize_in[] = {&arg_device, NULL};
|
|
|
|
/*
|
|
* AuthorizeService(o, s)
|
|
*/
|
|
|
|
static const GDBusArgInfo *authorize_service_in[] = {&arg_device, &arg_uuid,
|
|
NULL};
|
|
|
|
static const GDBusMethodInfo m_release = {-1, "Release", NULL, NULL, NULL};
|
|
|
|
static const GDBusMethodInfo m_req_pin = {-1, "RequestPinCode",
|
|
(GDBusArgInfo **)req_pin_in,
|
|
(GDBusArgInfo **)req_pin_out, NULL};
|
|
|
|
static const GDBusMethodInfo m_req_passkey = {
|
|
-1, "RequestPasskey", (GDBusArgInfo **)req_passkey_in,
|
|
(GDBusArgInfo **)req_passkey_out, NULL};
|
|
|
|
static const GDBusMethodInfo m_display_pin = {
|
|
-1, "DisplayPinCode", (GDBusArgInfo **)display_pin_in, NULL, NULL};
|
|
|
|
static const GDBusMethodInfo m_display_passkey = {
|
|
-1, "DisplayPasskey", (GDBusArgInfo **)display_passkey_in, NULL, NULL};
|
|
|
|
static const GDBusMethodInfo m_confirm = {
|
|
-1, "RequestConfirmation", (GDBusArgInfo **)confirm_in, NULL, NULL};
|
|
|
|
static const GDBusMethodInfo m_authorize = {
|
|
-1, "RequestAuthorization", (GDBusArgInfo **)authorize_in, NULL, NULL};
|
|
|
|
static const GDBusMethodInfo m_authorize_service = {
|
|
-1, "AuthorizeService", (GDBusArgInfo **)authorize_service_in, NULL, NULL};
|
|
|
|
static const GDBusMethodInfo m_cancel = {-1, "Cancel", NULL, NULL, NULL};
|
|
|
|
static const GDBusMethodInfo *agent_methods[] = {
|
|
&m_release, &m_req_pin, &m_req_passkey, &m_display_pin,
|
|
&m_display_passkey, &m_confirm, &m_authorize, &m_authorize_service,
|
|
&m_cancel, NULL};
|
|
|
|
static const GDBusInterfaceInfo agent_interface_info = {
|
|
-1, AGENT_INTERFACE, (GDBusMethodInfo **)agent_methods, NULL, NULL, NULL,
|
|
NULL};
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* stdin */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static gboolean on_stdin_input(GIOChannel *channel, GIOCondition cond,
|
|
gpointer user_data) {
|
|
gchar *line = NULL;
|
|
gsize len = 0;
|
|
GError *error = NULL;
|
|
GIOStatus status;
|
|
|
|
(void)user_data;
|
|
|
|
if (cond & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) {
|
|
|
|
stdin_watch_id = 0;
|
|
|
|
info("stdin closed");
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
status = g_io_channel_read_line(channel, &line, &len, NULL, &error);
|
|
|
|
if (status != G_IO_STATUS_NORMAL || !line) {
|
|
|
|
if (error) {
|
|
dbg("stdin read failed: %s", error->message);
|
|
|
|
g_error_free(error);
|
|
}
|
|
|
|
g_free(line);
|
|
|
|
if (status == G_IO_STATUS_EOF) {
|
|
stdin_watch_id = 0;
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
return G_SOURCE_CONTINUE;
|
|
}
|
|
|
|
g_strstrip(line);
|
|
|
|
if (g_str_has_prefix(line, "passkey")) {
|
|
|
|
const gchar *arg = line + strlen("passkey");
|
|
|
|
/*
|
|
* Require whitespace or end-of-line.
|
|
*/
|
|
if (*arg == '\0') {
|
|
|
|
info("usage: passkey <1-%d digits>", PASSKEY_MAX_LEN);
|
|
|
|
} else if (g_ascii_isspace(*arg)) {
|
|
|
|
set_passkey_from_command(arg);
|
|
|
|
} else {
|
|
|
|
info("unknown command, supported: passkey <1-%d digits>",
|
|
PASSKEY_MAX_LEN);
|
|
}
|
|
|
|
} else if (line[0] != '\0') {
|
|
|
|
info("unknown command, supported: passkey <1-%d digits>", PASSKEY_MAX_LEN);
|
|
}
|
|
|
|
g_free(line);
|
|
|
|
return G_SOURCE_CONTINUE;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Signal handling */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static gboolean on_signal_exit(gpointer user_data) {
|
|
GMainLoop *loop = user_data;
|
|
|
|
info("shutting down");
|
|
|
|
/*
|
|
* Unregister from BlueZ while the D-Bus connection is still alive.
|
|
*/
|
|
unregister_agent();
|
|
|
|
if (loop)
|
|
g_main_loop_quit(loop);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Cleanup */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void cleanup(void) {
|
|
/*
|
|
* Remove stdin watch.
|
|
*/
|
|
if (stdin_watch_id != 0) {
|
|
|
|
g_source_remove(stdin_watch_id);
|
|
|
|
stdin_watch_id = 0;
|
|
}
|
|
|
|
/*
|
|
* Remove BlueZ NameOwnerChanged subscription.
|
|
*/
|
|
if (dbus_conn && name_owner_sub_id != 0) {
|
|
|
|
g_dbus_connection_signal_unsubscribe(dbus_conn, name_owner_sub_id);
|
|
|
|
name_owner_sub_id = 0;
|
|
}
|
|
|
|
/*
|
|
* Unregister Agent from BlueZ.
|
|
*/
|
|
unregister_agent();
|
|
|
|
/*
|
|
* Unexport Agent1 object.
|
|
*/
|
|
if (dbus_conn && agent_reg_id != 0) {
|
|
|
|
g_dbus_connection_unregister_object(dbus_conn, agent_reg_id);
|
|
|
|
agent_reg_id = 0;
|
|
}
|
|
|
|
/*
|
|
* Close our reference to system bus connection.
|
|
*/
|
|
if (dbus_conn) {
|
|
|
|
g_object_unref(dbus_conn);
|
|
|
|
dbus_conn = NULL;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Options */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static const GOptionEntry options[] = {
|
|
|
|
{"capability", 'c', 0, G_OPTION_ARG_STRING, &opt_capability,
|
|
"Agent capability "
|
|
"(DisplayOnly, DisplayYesNo, KeyboardOnly, "
|
|
"NoInputNoOutput, KeyboardDisplay)",
|
|
"CAPABILITY"},
|
|
|
|
{"passkey", 'p', 0, G_OPTION_ARG_STRING, &opt_passkey,
|
|
"Fixed passkey for pairing (default 0000)", "PASSKEY"},
|
|
|
|
{"debug", 'd', 0, G_OPTION_ARG_NONE, &opt_debug, "Enable debug output",
|
|
NULL},
|
|
|
|
{NULL}};
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* main */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
int main(int argc, char *argv[]) {
|
|
GOptionContext *context;
|
|
GError *error = NULL;
|
|
|
|
main_loop = g_main_loop_new(NULL, FALSE);
|
|
|
|
/*
|
|
* Initialize default passkey.
|
|
*/
|
|
g_strlcpy(passkey, DEFAULT_PASSKEY, sizeof(passkey));
|
|
|
|
/*
|
|
* Parse command line.
|
|
*/
|
|
context = g_option_context_new("- standalone BlueZ auto-confirm agent");
|
|
|
|
g_option_context_add_main_entries(context, options, NULL);
|
|
|
|
if (!g_option_context_parse(context, &argc, &argv, &error)) {
|
|
|
|
err("option parsing failed: %s", error ? error->message : "unknown error");
|
|
|
|
if (error)
|
|
g_error_free(error);
|
|
|
|
g_option_context_free(context);
|
|
g_main_loop_unref(main_loop);
|
|
|
|
return 1;
|
|
}
|
|
|
|
if (!opt_capability) {
|
|
opt_capability = g_strdup(DEFAULT_CAPABILITY);
|
|
}
|
|
/*
|
|
* Validate capability.
|
|
*/
|
|
{
|
|
static const gchar *valid_caps[] = {"DisplayOnly", "DisplayYesNo",
|
|
"KeyboardOnly", "NoInputNoOutput",
|
|
"KeyboardDisplay", NULL};
|
|
|
|
gboolean ok = FALSE;
|
|
|
|
for (int i = 0; valid_caps[i]; i++) {
|
|
|
|
if (g_strcmp0(opt_capability, valid_caps[i]) == 0) {
|
|
|
|
ok = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
|
|
err("invalid capability '%s'", opt_capability);
|
|
|
|
g_option_context_free(context);
|
|
g_main_loop_unref(main_loop);
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Validate command-line passkey.
|
|
*/
|
|
if (opt_passkey) {
|
|
|
|
gchar *tmp = g_strdup(opt_passkey);
|
|
|
|
g_strstrip(tmp);
|
|
|
|
if (!set_passkey_internal(tmp, FALSE)) {
|
|
|
|
err("invalid passkey '%s', using default %s", tmp, DEFAULT_PASSKEY);
|
|
}
|
|
|
|
g_free(tmp);
|
|
}
|
|
|
|
info("starting");
|
|
info("capability=%s", opt_capability);
|
|
info("passkey=%s", passkey);
|
|
|
|
/*
|
|
* stdin command interface.
|
|
*
|
|
* Example:
|
|
*
|
|
* echo "passkey 123456" > /proc/<pid>/fd/0
|
|
*
|
|
* or type directly from a serial console.
|
|
*/
|
|
{
|
|
GIOChannel *stdin_channel;
|
|
|
|
stdin_channel = g_io_channel_unix_new(STDIN_FILENO);
|
|
|
|
g_io_channel_set_close_on_unref(stdin_channel, FALSE);
|
|
|
|
stdin_watch_id =
|
|
g_io_add_watch(stdin_channel, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
|
|
on_stdin_input, NULL);
|
|
|
|
g_io_channel_unref(stdin_channel);
|
|
}
|
|
|
|
/*
|
|
* Handle SIGINT/SIGTERM.
|
|
*/
|
|
g_unix_signal_add(SIGINT, on_signal_exit, main_loop);
|
|
|
|
g_unix_signal_add(SIGTERM, on_signal_exit, main_loop);
|
|
|
|
/*
|
|
* Connect system bus and setup Agent1.
|
|
*/
|
|
if (!setup_dbus()) {
|
|
|
|
cleanup();
|
|
|
|
g_option_context_free(context);
|
|
g_main_loop_unref(main_loop);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Keep D-Bus connection and Agent object alive.
|
|
*/
|
|
info("entering main loop");
|
|
|
|
g_main_loop_run(main_loop);
|
|
|
|
info("main loop exited");
|
|
|
|
/*
|
|
* Cleanup.
|
|
*/
|
|
cleanup();
|
|
|
|
g_free(opt_passkey);
|
|
opt_passkey = NULL;
|
|
|
|
g_free(opt_capability);
|
|
opt_capability = NULL;
|
|
|
|
g_option_context_free(context);
|
|
|
|
g_main_loop_unref(main_loop);
|
|
|
|
main_loop = NULL;
|
|
|
|
return 0;
|
|
}
|