2018-06-13 04:34:05 +08:00
|
|
|
#include <stdlib.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <time.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
2019-02-10 03:57:51 -05:00
|
|
|
#include <magisk.h>
|
|
|
|
#include <db.h>
|
|
|
|
#include <daemon.h>
|
2019-03-07 20:31:35 -05:00
|
|
|
#include <utils.h>
|
2018-06-13 04:34:05 +08:00
|
|
|
|
2019-11-14 00:01:06 -05:00
|
|
|
#define DB_VERSION 10
|
2018-10-28 14:49:04 -04:00
|
|
|
|
2019-03-06 08:16:12 -05:00
|
|
|
using namespace std;
|
2018-11-04 18:24:08 -05:00
|
|
|
|
2019-03-06 08:16:12 -05:00
|
|
|
static sqlite3 *mDB = nullptr;
|
2018-11-04 18:24:08 -05:00
|
|
|
|
2019-03-06 08:16:12 -05:00
|
|
|
int db_strings::getKeyIdx(string_view key) const {
|
2018-11-04 18:24:08 -05:00
|
|
|
int idx = DB_STRING_NUM;
|
|
|
|
for (int i = 0; i < DB_STRING_NUM; ++i) {
|
2019-03-06 08:16:12 -05:00
|
|
|
if (key == DB_STRING_KEYS[i])
|
2018-11-04 18:24:08 -05:00
|
|
|
idx = i;
|
|
|
|
}
|
|
|
|
return idx;
|
|
|
|
}
|
|
|
|
|
2019-03-06 08:16:12 -05:00
|
|
|
db_settings::db_settings() {
|
|
|
|
// Default settings
|
2019-03-06 08:21:23 -05:00
|
|
|
data[ROOT_ACCESS] = ROOT_ACCESS_APPS_AND_ADB;
|
|
|
|
data[SU_MULTIUSER_MODE] = MULTIUSER_MODE_OWNER_ONLY;
|
|
|
|
data[SU_MNT_NS] = NAMESPACE_MODE_REQUESTER;
|
2019-06-27 00:28:34 -07:00
|
|
|
data[HIDE_CONFIG] = true;
|
2018-11-04 18:24:08 -05:00
|
|
|
}
|
|
|
|
|
2019-03-06 08:16:12 -05:00
|
|
|
int db_settings::getKeyIdx(string_view key) const {
|
2018-11-04 18:24:08 -05:00
|
|
|
int idx = DB_SETTINGS_NUM;
|
|
|
|
for (int i = 0; i < DB_SETTINGS_NUM; ++i) {
|
2019-03-06 08:16:12 -05:00
|
|
|
if (key == DB_SETTING_KEYS[i])
|
2018-11-04 18:24:08 -05:00
|
|
|
idx = i;
|
|
|
|
}
|
|
|
|
return idx;
|
|
|
|
}
|
|
|
|
|
2018-11-04 03:38:06 -05:00
|
|
|
static int ver_cb(void *ver, int, char **data, char **) {
|
2019-03-07 20:31:35 -05:00
|
|
|
*((int *) ver) = parse_int(data[0]);
|
2018-06-13 04:34:05 +08:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-11-16 03:20:30 -05:00
|
|
|
#define err_ret(e) if (e) return e;
|
2018-10-28 14:49:04 -04:00
|
|
|
|
2018-11-16 03:20:30 -05:00
|
|
|
static char *open_and_init_db(sqlite3 *&db) {
|
|
|
|
int ret = sqlite3_open_v2(MAGISKDB, &db,
|
|
|
|
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr);
|
|
|
|
if (ret)
|
2018-12-05 12:48:01 -05:00
|
|
|
return strdup(sqlite3_errmsg(db));
|
2019-03-01 17:08:08 -05:00
|
|
|
int ver;
|
|
|
|
bool upgrade = false;
|
2018-10-28 14:49:04 -04:00
|
|
|
char *err;
|
|
|
|
sqlite3_exec(db, "PRAGMA user_version", ver_cb, &ver, &err);
|
2018-11-16 03:20:30 -05:00
|
|
|
err_ret(err);
|
2018-10-28 14:49:04 -04:00
|
|
|
if (ver > DB_VERSION) {
|
|
|
|
// Don't support downgrading database
|
2018-12-05 12:48:01 -05:00
|
|
|
sqlite3_close(db);
|
2018-11-04 03:38:06 -05:00
|
|
|
return nullptr;
|
2018-10-28 14:49:04 -04:00
|
|
|
}
|
2018-10-27 17:54:48 -04:00
|
|
|
if (ver < 3) {
|
|
|
|
// Policies
|
|
|
|
sqlite3_exec(db,
|
2019-03-06 05:40:52 -05:00
|
|
|
"CREATE TABLE IF NOT EXISTS policies "
|
|
|
|
"(uid INT, package_name TEXT, policy INT, until INT, "
|
|
|
|
"logging INT, notification INT, PRIMARY KEY(uid))",
|
|
|
|
nullptr, nullptr, &err);
|
2018-11-16 03:20:30 -05:00
|
|
|
err_ret(err);
|
2018-10-27 17:54:48 -04:00
|
|
|
// Settings
|
|
|
|
sqlite3_exec(db,
|
2019-03-06 05:40:52 -05:00
|
|
|
"CREATE TABLE IF NOT EXISTS settings "
|
|
|
|
"(key TEXT, value INT, PRIMARY KEY(key))",
|
|
|
|
nullptr, nullptr, &err);
|
2018-11-16 03:20:30 -05:00
|
|
|
err_ret(err);
|
2018-10-27 17:54:48 -04:00
|
|
|
ver = 3;
|
2019-03-01 17:08:08 -05:00
|
|
|
upgrade = true;
|
2018-10-27 17:54:48 -04:00
|
|
|
}
|
2019-03-01 17:08:08 -05:00
|
|
|
if (ver < 4) {
|
2018-10-27 17:54:48 -04:00
|
|
|
// Strings
|
|
|
|
sqlite3_exec(db,
|
2019-03-06 05:40:52 -05:00
|
|
|
"CREATE TABLE IF NOT EXISTS strings "
|
|
|
|
"(key TEXT, value TEXT, PRIMARY KEY(key))",
|
|
|
|
nullptr, nullptr, &err);
|
2018-11-16 03:20:30 -05:00
|
|
|
err_ret(err);
|
2018-10-27 17:54:48 -04:00
|
|
|
ver = 4;
|
2019-03-01 17:08:08 -05:00
|
|
|
upgrade = true;
|
2018-10-27 17:54:48 -04:00
|
|
|
}
|
2019-03-01 17:08:08 -05:00
|
|
|
if (ver < 5) {
|
2018-11-04 03:38:06 -05:00
|
|
|
sqlite3_exec(db, "UPDATE policies SET uid=uid%100000", nullptr, nullptr, &err);
|
2018-11-16 03:20:30 -05:00
|
|
|
err_ret(err);
|
2019-03-06 05:40:52 -05:00
|
|
|
/* Directly jump to version 6 */
|
2018-10-27 17:54:48 -04:00
|
|
|
ver = 6;
|
2019-03-01 17:08:08 -05:00
|
|
|
upgrade = true;
|
2018-10-27 17:54:48 -04:00
|
|
|
}
|
2019-03-01 17:08:08 -05:00
|
|
|
if (ver < 7) {
|
2018-11-01 13:23:12 -04:00
|
|
|
sqlite3_exec(db,
|
2019-03-06 05:40:52 -05:00
|
|
|
"CREATE TABLE IF NOT EXISTS hidelist "
|
|
|
|
"(package_name TEXT, process TEXT, PRIMARY KEY(package_name, process));",
|
|
|
|
nullptr, nullptr, &err);
|
2018-11-16 03:20:30 -05:00
|
|
|
err_ret(err);
|
2019-03-06 05:40:52 -05:00
|
|
|
/* Directly jump to version 9 */
|
|
|
|
ver = 9;
|
2019-03-01 17:08:08 -05:00
|
|
|
upgrade = true;
|
|
|
|
}
|
|
|
|
if (ver < 8) {
|
|
|
|
sqlite3_exec(db,
|
2019-03-06 05:40:52 -05:00
|
|
|
"BEGIN TRANSACTION;"
|
|
|
|
"ALTER TABLE hidelist RENAME TO hidelist_tmp;"
|
|
|
|
"CREATE TABLE IF NOT EXISTS hidelist "
|
|
|
|
"(package_name TEXT, process TEXT, PRIMARY KEY(package_name, process));"
|
|
|
|
"INSERT INTO hidelist SELECT process as package_name, process FROM hidelist_tmp;"
|
|
|
|
"DROP TABLE hidelist_tmp;"
|
|
|
|
"COMMIT;",
|
|
|
|
nullptr, nullptr, &err);
|
2019-03-01 17:08:08 -05:00
|
|
|
err_ret(err);
|
2019-03-06 05:40:52 -05:00
|
|
|
/* Directly jump to version 9 */
|
|
|
|
ver = 9;
|
|
|
|
upgrade = true;
|
|
|
|
}
|
|
|
|
if (ver < 9) {
|
|
|
|
sqlite3_exec(db,
|
|
|
|
"BEGIN TRANSACTION;"
|
|
|
|
"ALTER TABLE hidelist RENAME TO hidelist_tmp;"
|
|
|
|
"CREATE TABLE IF NOT EXISTS hidelist "
|
|
|
|
"(package_name TEXT, process TEXT, PRIMARY KEY(package_name, process));"
|
|
|
|
"INSERT INTO hidelist SELECT * FROM hidelist_tmp;"
|
|
|
|
"DROP TABLE hidelist_tmp;"
|
|
|
|
"COMMIT;",
|
|
|
|
nullptr, nullptr, &err);
|
|
|
|
err_ret(err);
|
|
|
|
ver = 9;
|
2019-03-01 17:08:08 -05:00
|
|
|
upgrade = true;
|
2018-11-01 13:23:12 -04:00
|
|
|
}
|
2019-11-14 00:01:06 -05:00
|
|
|
if (ver < 10) {
|
2019-11-21 14:40:12 -05:00
|
|
|
sqlite3_exec(db, "DROP TABLE IF EXISTS logs", nullptr, nullptr, &err);
|
2019-11-14 00:01:06 -05:00
|
|
|
err_ret(err);
|
|
|
|
ver = 10;
|
|
|
|
upgrade = true;
|
|
|
|
}
|
2018-10-27 17:54:48 -04:00
|
|
|
|
|
|
|
if (upgrade) {
|
|
|
|
// Set version
|
|
|
|
char query[32];
|
|
|
|
sprintf(query, "PRAGMA user_version=%d", ver);
|
2018-11-04 03:38:06 -05:00
|
|
|
sqlite3_exec(db, query, nullptr, nullptr, &err);
|
2018-11-16 03:20:30 -05:00
|
|
|
err_ret(err);
|
2018-10-28 14:49:04 -04:00
|
|
|
}
|
2018-11-16 03:20:30 -05:00
|
|
|
return nullptr;
|
2018-10-28 14:49:04 -04:00
|
|
|
}
|
|
|
|
|
2019-03-06 08:16:12 -05:00
|
|
|
char *db_exec(const char *sql) {
|
2018-11-16 03:20:30 -05:00
|
|
|
char *err;
|
|
|
|
if (mDB == nullptr) {
|
|
|
|
err = open_and_init_db(mDB);
|
|
|
|
db_err_cmd(err,
|
|
|
|
// Open fails, remove and reconstruct
|
|
|
|
unlink(MAGISKDB);
|
|
|
|
err = open_and_init_db(mDB);
|
|
|
|
err_ret(err);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (mDB) {
|
2019-03-06 08:16:12 -05:00
|
|
|
sqlite3_exec(mDB, sql, nullptr, nullptr, &err);
|
2018-11-16 03:20:30 -05:00
|
|
|
return err;
|
2018-10-27 17:54:48 -04:00
|
|
|
}
|
2018-11-16 03:20:30 -05:00
|
|
|
return nullptr;
|
2018-10-27 17:54:48 -04:00
|
|
|
}
|
|
|
|
|
2019-03-06 08:16:12 -05:00
|
|
|
char *db_exec(const char *sql, const db_row_cb &fn) {
|
|
|
|
char *err;
|
|
|
|
if (mDB == nullptr) {
|
|
|
|
err = open_and_init_db(mDB);
|
|
|
|
db_err_cmd(err,
|
|
|
|
// Open fails, remove and reconstruct
|
|
|
|
unlink(MAGISKDB);
|
|
|
|
err = open_and_init_db(mDB);
|
|
|
|
err_ret(err);
|
|
|
|
);
|
2018-06-13 04:34:05 +08:00
|
|
|
}
|
2019-03-06 08:16:12 -05:00
|
|
|
if (mDB) {
|
|
|
|
sqlite3_exec(mDB, sql, [](void *cb, int col_num, char **data, char **col_name) -> int {
|
|
|
|
auto &func = *reinterpret_cast<const db_row_cb*>(cb);
|
|
|
|
db_row row;
|
|
|
|
for (int i = 0; i < col_num; ++i)
|
|
|
|
row[col_name[i]] = data[i];
|
|
|
|
return func(row) ? 0 : 1;
|
|
|
|
}, (void *) &fn, &err);
|
|
|
|
return err;
|
2018-06-13 04:34:05 +08:00
|
|
|
}
|
2019-03-06 08:16:12 -05:00
|
|
|
return nullptr;
|
2018-06-13 04:34:05 +08:00
|
|
|
}
|
|
|
|
|
2019-03-06 08:16:12 -05:00
|
|
|
int get_db_settings(db_settings &cfg, int key) {
|
2018-10-27 17:54:48 -04:00
|
|
|
char *err;
|
2019-03-06 08:16:12 -05:00
|
|
|
auto settings_cb = [&](db_row &row) -> bool {
|
2019-03-07 20:31:35 -05:00
|
|
|
cfg[row["key"]] = parse_int(row["value"]);
|
2019-03-06 08:16:12 -05:00
|
|
|
LOGD("magiskdb: query %s=[%s]\n", row["key"].data(), row["value"].data());
|
|
|
|
return true;
|
|
|
|
};
|
2018-11-20 01:50:31 -05:00
|
|
|
if (key >= 0) {
|
2018-10-27 17:54:48 -04:00
|
|
|
char query[128];
|
2018-10-27 17:56:20 -04:00
|
|
|
sprintf(query, "SELECT key, value FROM settings WHERE key='%s'", DB_SETTING_KEYS[key]);
|
2019-03-06 08:16:12 -05:00
|
|
|
err = db_exec(query, settings_cb);
|
2018-10-27 17:54:48 -04:00
|
|
|
} else {
|
2019-03-06 08:16:12 -05:00
|
|
|
err = db_exec("SELECT key, value FROM settings", settings_cb);
|
2018-10-27 17:54:48 -04:00
|
|
|
}
|
2018-11-16 03:20:30 -05:00
|
|
|
db_err_cmd(err, return 1);
|
2018-10-27 17:54:48 -04:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-03-06 08:16:12 -05:00
|
|
|
int get_db_strings(db_strings &str, int key) {
|
2018-06-13 04:34:05 +08:00
|
|
|
char *err;
|
2019-03-06 08:16:12 -05:00
|
|
|
auto string_cb = [&](db_row &row) -> bool {
|
|
|
|
str[row["key"]] = row["value"];
|
|
|
|
return true;
|
|
|
|
};
|
2018-11-20 01:50:31 -05:00
|
|
|
if (key >= 0) {
|
2018-06-13 04:34:05 +08:00
|
|
|
char query[128];
|
2018-10-27 17:56:20 -04:00
|
|
|
sprintf(query, "SELECT key, value FROM strings WHERE key='%s'", DB_STRING_KEYS[key]);
|
2019-03-06 08:16:12 -05:00
|
|
|
err = db_exec(query, string_cb);
|
2018-06-13 04:34:05 +08:00
|
|
|
} else {
|
2019-03-06 08:16:12 -05:00
|
|
|
err = db_exec("SELECT key, value FROM strings", string_cb);
|
2018-06-13 04:34:05 +08:00
|
|
|
}
|
2019-03-06 08:16:12 -05:00
|
|
|
db_err_cmd(err, return 1);
|
2018-06-13 04:34:05 +08:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-05-13 02:01:10 -07:00
|
|
|
int get_uid_policy(su_access &su, int uid) {
|
2018-06-13 04:34:05 +08:00
|
|
|
char query[256], *err;
|
|
|
|
sprintf(query, "SELECT policy, logging, notification FROM policies "
|
2018-11-04 03:38:06 -05:00
|
|
|
"WHERE uid=%d AND (until=0 OR until>%li)", uid, time(nullptr));
|
2019-03-06 08:16:12 -05:00
|
|
|
err = db_exec(query, [&](db_row &row) -> bool {
|
2019-03-07 20:31:35 -05:00
|
|
|
su.policy = (policy_t) parse_int(row["policy"]);
|
|
|
|
su.log = parse_int(row["logging"]);
|
|
|
|
su.notify = parse_int(row["notification"]);
|
2019-03-06 08:16:12 -05:00
|
|
|
LOGD("magiskdb: query policy=[%d] log=[%d] notify=[%d]\n", su.policy, su.log, su.notify);
|
|
|
|
return true;
|
|
|
|
});
|
2018-11-16 03:20:30 -05:00
|
|
|
db_err_cmd(err, return 1);
|
2018-06-13 04:34:05 +08:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-12-17 16:38:12 -05:00
|
|
|
bool check_manager(string *pkg) {
|
|
|
|
db_strings str;
|
|
|
|
get_db_strings(str, SU_MANAGER);
|
|
|
|
bool ret = validate_manager(str[SU_MANAGER], 0, nullptr);
|
|
|
|
if (pkg) {
|
|
|
|
if (ret)
|
|
|
|
pkg->swap(str[SU_MANAGER]);
|
|
|
|
else
|
|
|
|
*pkg = "xxx"; /* Make sure the return pkg can never exist */
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool validate_manager(string &pkg, int userid, struct stat *st) {
|
2019-03-06 08:16:12 -05:00
|
|
|
struct stat tmp_st;
|
|
|
|
if (st == nullptr)
|
|
|
|
st = &tmp_st;
|
|
|
|
|
2018-06-13 04:34:05 +08:00
|
|
|
// Prefer DE storage
|
|
|
|
char app_path[128];
|
2019-12-17 16:38:12 -05:00
|
|
|
sprintf(app_path, "%s/%d/%s", APP_DATA_DIR, userid, pkg.data());
|
|
|
|
if (pkg.empty() || stat(app_path, st)) {
|
2018-06-13 04:34:05 +08:00
|
|
|
// Check the official package name
|
2019-06-03 23:32:49 -07:00
|
|
|
sprintf(app_path, "%s/%d/" JAVA_PACKAGE_NAME, APP_DATA_DIR, userid);
|
2018-06-13 04:34:05 +08:00
|
|
|
if (stat(app_path, st)) {
|
|
|
|
LOGE("su: cannot find manager");
|
|
|
|
memset(st, 0, sizeof(*st));
|
2019-12-17 16:38:12 -05:00
|
|
|
pkg.clear();
|
|
|
|
return false;
|
2018-06-13 04:34:05 +08:00
|
|
|
} else {
|
|
|
|
// Switch to official package if exists
|
2019-12-17 16:38:12 -05:00
|
|
|
pkg = JAVA_PACKAGE_NAME;
|
2018-06-13 04:34:05 +08:00
|
|
|
}
|
|
|
|
}
|
2019-12-17 16:38:12 -05:00
|
|
|
return true;
|
2018-06-13 04:34:05 +08:00
|
|
|
}
|
2018-10-27 17:54:48 -04:00
|
|
|
|
2018-11-16 03:20:30 -05:00
|
|
|
void exec_sql(int client) {
|
Introduce component agnostic communication
Usually, the communication between native and the app is done via
sending intents to either broadcast or activity. These communication
channels are for launching root requests dialogs, sending root request
notifications (the toast you see when an app gained root access), and
root request logging.
Sending intents by am (activity manager) usually requires specifying
the component name in the format of <pkg>/<class name>. This means parts
of Magisk Manager cannot be randomized or else the native daemon is
unable to know where to send data to the app.
On modern Android (not sure which API is it introduced), it is possible
to send broadcasts to a package, not a specific component. Which
component will receive the intent depends on the intent filter declared
in AndroidManifest.xml. Since we already have a mechanism in native code
to keep track of the package name of Magisk Manager, this makes it
perfect to pass intents to Magisk Manager that have components being
randomly obfuscated (stub APKs).
There are a few caveats though. Although this broadcasting method works
perfectly fine on AOSP and most systems, there are OEMs out there
shipping ROMs blocking broadcasts unexpectedly. In order to make sure
Magisk works in all kinds of scenarios, we run actual tests every boot
to determine which communication method should be used.
We have 3 methods in total, ordered in preference:
1. Broadcasting to a package
2. Broadcasting to a specific component
3. Starting a specific activity component
Method 3 will always work on any device, but the downside is anytime
a communication happens, Magisk Manager will steal foreground focus
regardless of whether UI is drawn. Method 1 is the only way to support
obfuscated stub APKs. The communication test will test method 1 and 2,
and if Magisk Manager is able to receive the messages, it will then
update the daemon configuration to use whichever is preferable. If none
of the broadcasts can be delivered, then the fallback method 3 will be
used.
2019-10-21 13:59:04 -04:00
|
|
|
run_finally f([=]{ close(client); });
|
2018-11-16 03:20:30 -05:00
|
|
|
char *sql = read_string(client);
|
2019-03-06 08:16:12 -05:00
|
|
|
char *err = db_exec(sql, [&](db_row &row) -> bool {
|
2019-09-01 13:58:50 +08:00
|
|
|
string out;
|
|
|
|
bool first = true;
|
2019-03-06 08:16:12 -05:00
|
|
|
for (auto it : row) {
|
2019-09-01 13:58:50 +08:00
|
|
|
if (first) first = false;
|
|
|
|
else out += '|';
|
|
|
|
out += it.first;
|
|
|
|
out += '=';
|
|
|
|
out += it.second;
|
2019-03-06 08:16:12 -05:00
|
|
|
}
|
2019-09-01 13:58:50 +08:00
|
|
|
write_int(client, out.length());
|
|
|
|
xwrite(client, out.data(), out.length());
|
2019-03-06 08:16:12 -05:00
|
|
|
return true;
|
|
|
|
});
|
2018-11-16 03:20:30 -05:00
|
|
|
free(sql);
|
Introduce component agnostic communication
Usually, the communication between native and the app is done via
sending intents to either broadcast or activity. These communication
channels are for launching root requests dialogs, sending root request
notifications (the toast you see when an app gained root access), and
root request logging.
Sending intents by am (activity manager) usually requires specifying
the component name in the format of <pkg>/<class name>. This means parts
of Magisk Manager cannot be randomized or else the native daemon is
unable to know where to send data to the app.
On modern Android (not sure which API is it introduced), it is possible
to send broadcasts to a package, not a specific component. Which
component will receive the intent depends on the intent filter declared
in AndroidManifest.xml. Since we already have a mechanism in native code
to keep track of the package name of Magisk Manager, this makes it
perfect to pass intents to Magisk Manager that have components being
randomly obfuscated (stub APKs).
There are a few caveats though. Although this broadcasting method works
perfectly fine on AOSP and most systems, there are OEMs out there
shipping ROMs blocking broadcasts unexpectedly. In order to make sure
Magisk works in all kinds of scenarios, we run actual tests every boot
to determine which communication method should be used.
We have 3 methods in total, ordered in preference:
1. Broadcasting to a package
2. Broadcasting to a specific component
3. Starting a specific activity component
Method 3 will always work on any device, but the downside is anytime
a communication happens, Magisk Manager will steal foreground focus
regardless of whether UI is drawn. Method 1 is the only way to support
obfuscated stub APKs. The communication test will test method 1 and 2,
and if Magisk Manager is able to receive the messages, it will then
update the daemon configuration to use whichever is preferable. If none
of the broadcasts can be delivered, then the fallback method 3 will be
used.
2019-10-21 13:59:04 -04:00
|
|
|
write_int(client, 0);
|
|
|
|
db_err_cmd(err, return; );
|
2018-10-27 17:54:48 -04:00
|
|
|
}
|