/* * Copyright 2017 IoT.bzh * * author: Loïc Collignon <loic.collignon@iot.bzh> * author: Jose Bollo <jose.bollo@iot.bzh> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #define _GNU_SOURCE #include <unistd.h> #include <sys/types.h> #include <pwd.h> #include <stdio.h> #include <string.h> #include <limits.h> #include <json-c/json.h> #define AFB_BINDING_VERSION 2 #include <afb/afb-binding.h> #if !defined(TO_STRING_FLAGS) # if !defined(JSON_C_TO_STRING_NOSLASHESCAPE) # define JSON_C_TO_STRING_NOSLASHESCAPE (1<<4) # endif # define TO_STRING_FLAGS (JSON_C_TO_STRING_PLAIN | JSON_C_TO_STRING_NOSLASHESCAPE) #endif #if defined(USE_BERKELEY_DB) # undef USE_BERKELEY_DB #endif #if defined(USE_GDBM) # undef USE_GDBM # define USE_GDBM 1 # define USE_BERKELEY_DB 0 #else # define USE_GDBM 0 # define USE_BERKELEY_DB 1 #endif // ----- Berkeley database ----- #if USE_BERKELEY_DB #include <db.h> #define DBFILE "ll-database-binding.db" #define DATA DBT #define DATA_SET(k,d,s) do{memset((k),0,sizeof*(k));(k)->data=(void*)d;(k)->size=(uint32_t)s;}while(0) #define DATA_PTR(k) ((void*)((k).data)) #define DATA_STR(k) ((char*)((k).data)) #define DATA_SZ(k) ((size_t)((k).size)) static DB *database; static int xdb_open(const char *path) { int ret; ret = db_create(&database, NULL, 0); if (ret != 0) { AFB_ERROR("Failed to create database: %s.", db_strerror(ret)); return -1; } ret = database->open(database, NULL, path, NULL, DB_BTREE, DB_CREATE, 0600); if (ret != 0) { AFB_ERROR("Failed to open the '%s' database: %s.", path, db_strerror(ret)); database->close(database, 0); return -1; } return 0; } static void xdb_put(struct afb_req req, DBT *key, DBT *data, int replace) { int ret; ret = database->put(database, NULL, key, data, replace ? 0 : DB_NOOVERWRITE); if (ret == 0) afb_req_success(req, NULL, NULL); else { AFB_ERROR("can't %s key %s with %s", replace ? "replace" : "insert", DATA_STR(*key), DATA_STR(*data)); afb_req_fail_f(req, "failed", "%s", db_strerror(ret)); } } static void xdb_delete(struct afb_req req, DBT *key) { int ret; ret = database->del(database, NULL, key, 0); if (ret == 0) afb_req_success_f(req, NULL, NULL); else { AFB_ERROR("can't delete key %s", DATA_STR(*key)); afb_req_fail_f(req, "failed", "%s", db_strerror(ret)); } free(DATA_PTR(key)); } static void verb_read(struct afb_req req) { DATA key; DATA data; int ret; char value[4096]; struct json_object* result; struct json_object* val; if (get_key(req, &key)) return; AFB_INFO("read: key=%s", DATA_STR(key)); memset(&data, 0, sizeof data); data.data = value; data.ulen = 4096; data.flags = DB_DBT_USERMEM; ret = database->get(database, NULL, &key, &data, 0); if (ret == 0) { result = json_object_new_object(); val = json_tokener_parse(DATA_STR(data)); json_object_object_add(result, "value", val ? val : json_object_new_string(DATA_STR(data))); afb_req_success_f(req, result, "db success: read %s=%s.", DATA_STR(key), DATA_STR(data)); } else afb_req_fail_f(req, "Failed to read datas.", "db fail: read %s - %s", DATA_STR(key), db_strerror(ret)); free(DATA_PTR(key)); } #endif // ----- gdbm database ----- #if USE_GDBM #include <errno.h> #include <gdbm.h> #define DBFILE "ll-database-binding.dbm" #define DATA datum #define DATA_SET(k,d,s) do{(k)->dptr=(char*)d;(k)->dsize=(int)s;}while(0) #define DATA_PTR(k) ((void*)((k).dptr)) #define DATA_STR(k) ((char*)((k).dptr)) #define DATA_SZ(k) ((size_t)((k).dsize)) #if GDBM_VERSION_MAJOR > 1 || (GDBM_VERSION_MAJOR == 1 && GDBM_VERSION_MINOR >= 13) # define IFSYS(yes,no) (gdbm_syserr[gdbm_errno] ? (yes) : (no)) #else # define IFSYS(yes,no) (no) #endif static GDBM_FILE database; static void onfatal(const char *text) { AFB_ERROR("fatal gdbm message: %s", text); } static int xdb_open(const char *path) { database = gdbm_open(path, 512, GDBM_WRCREAT|GDBM_SYNC, 0600, onfatal); if (!database) { AFB_ERROR("Fail to open/create database: %s%s%s", gdbm_strerror(gdbm_errno), IFSYS(", ", ""), IFSYS(strerror(errno), "")); return -1; } return 0; } static void xdb_put(struct afb_req req, datum *key, datum *data, int replace) { int ret; ret = gdbm_store(database, *key, *data, replace ? GDBM_REPLACE : GDBM_INSERT); if (ret == 0) afb_req_success(req, NULL, NULL); else { AFB_ERROR("can't %s key %s with %s: %s%s%s", replace ? "replace" : "insert", DATA_STR(*key), DATA_STR(*data), gdbm_strerror(gdbm_errno), IFSYS(", ", ""), IFSYS(strerror(errno), "")); afb_req_fail_f(req, "failed", "%s", ret > 0 ? "key already exists" : gdbm_strerror(gdbm_errno)); } } static void xdb_delete(struct afb_req req, datum *key) { int ret; ret = gdbm_delete(database, *key); if (ret == 0) afb_req_success_f(req, NULL, NULL); else { AFB_ERROR("can't delete key %s: %s%s%s", DATA_STR(*key), gdbm_strerror(gdbm_errno), IFSYS(", ", ""), IFSYS(strerror(errno), "")); afb_req_fail_f(req, "failed", "%s", gdbm_strerror(gdbm_errno)); } } static void xdb_get(struct afb_req req, datum *key) { struct json_object* obj; datum result; result = gdbm_fetch(database, *key); if (result.dptr) { obj = json_object_new_object(); json_object_object_add(obj, "value", json_tokener_parse(result.dptr)); afb_req_success(req, obj, NULL); free(result.dptr); } else { AFB_ERROR("can't get key %s: %s%s%s", DATA_STR(*key), gdbm_strerror(gdbm_errno), IFSYS(", ", ""), IFSYS(strerror(errno), "")); afb_req_fail_f(req, "failed", "%s", gdbm_strerror(gdbm_errno)); } } #endif // ----- Binding's implementations ----- /** * @brief Get the path to the database */ static int get_database_path(char *buffer, size_t size) { static const char dbfile[] = DBFILE; char *home, *config; int rc; config = secure_getenv("XDG_CONFIG_HOME"); if (config) rc = snprintf(buffer, size, "%s/%s", config, dbfile); else { home = secure_getenv("HOME"); if (home) rc = snprintf(buffer, size, "%s/.config/%s", home, dbfile); else { uid_t uid = getuid(); struct passwd *pwd = getpwuid(uid); if (pwd) rc = snprintf(buffer, size, "%s/.config/%s", pwd->pw_dir, dbfile); else rc = snprintf(buffer, size, "/home/%d/.config/%s", (int)uid, dbfile); } } return rc; } /** * @brief Initialize the binding. * @return Exit code, zero if success. */ static int ll_database_binding_init() { char path[PATH_MAX]; int ret; ret = get_database_path(path, sizeof path); if (ret < 0 || (int)ret >= (int)(sizeof path)) { AFB_ERROR("Can't compute the database filename"); return -1; } AFB_INFO("opening database %s", path); return xdb_open(path); } /** * Returns the database key for the 'req' */ static int get_key(struct afb_req req, DATA *key) { char *appid, *data; const char *jkey; size_t ljkey, lappid, size; struct json_object* args; struct json_object* item; /* get the key */ args = afb_req_json(req); if (!json_object_object_get_ex(args, "key", &item)) { afb_req_fail(req, "no-key", NULL); return -1; } if (!item || !(jkey = json_object_to_json_string_ext(item, JSON_C_TO_STRING_PLAIN)) || !(ljkey = strlen(jkey))) { afb_req_fail(req, "bad-key", NULL); return -1; } /* get the appid */ appid = afb_req_get_application_id(req); #if 1 if (!appid) appid = strdup("#UNKNOWN-APP#"); #endif if (!appid) { afb_req_fail(req, "bad-context", NULL); return -1; } /* make the db-key */ lappid = strlen(appid); size = lappid + ljkey + 2; data = realloc(appid, size); if (!data) { free(appid); afb_req_fail(req, "out-of-memory", NULL); return -1; } data[lappid] = ':'; memcpy(&data[lappid + 1], jkey, ljkey + 1); /* return the key */ DATA_SET(key, data, size); return 0; } static void put(struct afb_req req, int replace) { DATA key; DATA data; const char* value; struct json_object* args; struct json_object* item; /* get the value */ args = afb_req_json(req); if (!json_object_object_get_ex(args, "value", &item)) { afb_req_fail(req, "no-value", NULL); return; } value = json_object_to_json_string_ext(item, TO_STRING_FLAGS); if (!value) { afb_req_fail(req, "out-of-memory", NULL); return; } DATA_SET(&data, value, strlen(value) + 1); /* includes the tailing null */ /* get the key */ if (get_key(req, &key)) return; AFB_INFO("put: key=%s, value=%s", DATA_STR(key), DATA_STR(data)); xdb_put(req, &key, &data, replace); free(DATA_PTR(key)); } static void verb_insert(struct afb_req req) { put(req, 0); } static void verb_update(struct afb_req req) { put(req, 1); } static void verb_delete(struct afb_req req) { DATA key; if (get_key(req, &key)) return; AFB_INFO("delete: key=%s", DATA_STR(key)); xdb_delete(req, &key); free(DATA_PTR(key)); } static void verb_read(struct afb_req req) { DATA key; if (get_key(req, &key)) return; AFB_INFO("read: key=%s", DATA_STR(key)); xdb_get(req, &key); free(DATA_PTR(key)); } // ----- Binding's configuration ----- /* static const struct afb_auth ll_database_binding_auths[] = { }; */ #define VERB(name_,auth_,info_,sess_) {\ .verb = #name_, \ .callback = verb_##name_, \ .auth = auth_, \ .info = info_, \ .session = sess_ } static const afb_verb_v2 ll_database_binding_verbs[]= { VERB(insert, NULL, NULL, AFB_SESSION_NONE_V2), VERB(update, NULL, NULL, AFB_SESSION_NONE_V2), VERB(delete, NULL, NULL, AFB_SESSION_NONE_V2), VERB(read, NULL, NULL, AFB_SESSION_NONE_V2), { .verb = NULL} }; const struct afb_binding_v2 afbBindingV2 = { .api = "persistence", .specification = NULL, .verbs = ll_database_binding_verbs, .preinit = NULL, .init = ll_database_binding_init, .onevent = NULL, .noconcurrency = 0 };