/* * OwnTracks Recorder * Copyright (C) 2015-2016 Jan-Piet Mens * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #ifdef WITH_LUA # include # include # include "util.h" # include "udata.h" # include "hooks.h" # include # include # include # include "fences.h" # include "gcache.h" # include "json.h" # include "version.h" # include "fences.h" static int otr_log(lua_State *lua); static int otr_strftime(lua_State *lua); static int otr_putdb(lua_State *lua); static int otr_getdb(lua_State *lua); #ifdef WITH_MQTT # include static int otr_publish(lua_State *lua); static struct mosquitto *MQTTconn = NULL; #endif static struct gcache *LuaDB = NULL; /* * Invoke the function `name' in the Lua script, which _may_ return * an integer that we, in turn, return to caller. */ static int l_function(lua_State *L, char *name) { int rc = 0; lua_getglobal(L, name); if (lua_type(L, -1) != LUA_TFUNCTION) { olog(LOG_ERR, "Cannot invoke Lua function %s: %s\n", name, lua_tostring(L, -1)); rc = 1; } else { lua_call(L, 0, 1); rc = (int)lua_tonumber(L, -1); lua_pop(L, 1); } lua_settop(L, 0); return (rc); } struct luadata *hooks_init(struct udata *ud, char *script) { struct luadata *ld; int rc; if ((ld = malloc(sizeof(struct luadata))) == NULL) return (NULL); ld->script = strdup(script); // ld->L = lua_open(); ld->L = luaL_newstate(); luaL_openlibs(ld->L); MQTTconn = ud->mosq; /* * Set up a global with some values. */ lua_newtable(ld->L); lua_pushstring(ld->L, VERSION); lua_setfield(ld->L, -2, "version"); // lua_pushstring(ld->L, "/Users/jpm/Auto/projects/on-github/owntracks/recorder/lua"); // lua_setfield(ld->L, -2, "luapath"); lua_pushcfunction(ld->L, otr_log); lua_setfield(ld->L, -2, "log"); lua_pushcfunction(ld->L, otr_strftime); lua_setfield(ld->L, -2, "strftime"); lua_pushcfunction(ld->L, otr_putdb); lua_setfield(ld->L, -2, "putdb"); lua_pushcfunction(ld->L, otr_getdb); lua_setfield(ld->L, -2, "getdb"); #ifdef WITH_MQTT lua_pushcfunction(ld->L, otr_publish); lua_setfield(ld->L, -2, "publish"); #endif lua_setglobal(ld->L, "otr"); LuaDB = ud->luadb; olog(LOG_DEBUG, "initializing Lua hooks from `%s'", script); /* Load the Lua script */ if (luaL_dofile(ld->L, ld->script)) { olog(LOG_ERR, "Cannot load Lua from %s: %s", ld->script, lua_tostring(ld->L, -1)); hooks_exit(ld, "failed to load script"); return (NULL); } rc = l_function(ld->L, "otr_init"); if (rc != 0) { /* * After all this work (sigh), the Script has decided we shouldn't use * hooks, so unload the whole Lua stuff and NULL back. */ hooks_exit(ld, "otr_init() returned non-zero"); ld = NULL; } return (ld); } void hooks_exit(struct luadata *ld, char *reason) { if (ld && ld->script) { l_function(ld->L, "otr_exit"); olog(LOG_DEBUG, "unloading Lua: %s", reason); free(ld->script); lua_close(ld->L); free(ld); } } static void do_hook(char *hookname, struct udata *ud, char *topic, JsonNode *fullo) { struct luadata *ld = ud->luadata; char *_type = "unknown"; JsonNode *j; if (!ld || !ld->script) return; lua_settop(ld->L, 0); lua_getglobal(ld->L, hookname); if (lua_type(ld->L, -1) != LUA_TFUNCTION) { olog(LOG_NOTICE, "cannot invoke %s in Lua script", hookname); return; } lua_pushstring(ld->L, topic); /* arg1: topic */ if ((j = json_find_member(fullo, "_type")) != NULL) { if (j->tag == JSON_STRING) _type = j->string_; } lua_pushstring(ld->L, _type); /* arg2: record type */ lua_newtable(ld->L); /* arg3: table */ json_foreach(j, fullo) { if (j->tag >= JSON_ARRAY) continue; lua_pushstring(ld->L, j->key); /* table key */ if (j->tag == JSON_STRING) { lua_pushstring(ld->L, j->string_); } else if (j->tag == JSON_NUMBER) { lua_pushnumber(ld->L, j->number_); } else if (j->tag == JSON_NULL) { lua_pushnil(ld->L); } else if (j->tag == JSON_BOOL) { lua_pushboolean(ld->L, j->bool_); } lua_rawset(ld->L, -3); } /* Invoke `hook' function in Lua with our args */ if (lua_pcall(ld->L, 3, 1, 0)) { olog(LOG_ERR, "Failed to run script: %s", lua_tostring(ld->L, -1)); exit(1); } // rc = (int)lua_tonumber(ld->L, -1); // printf("C: FILTER returns %d\n", rc); } static void hooks_hooklet(struct udata *ud, char *topic, JsonNode *fullo) { JsonNode *j; struct luadata *ld = ud->luadata; char hookname[BUFSIZ]; if (!ud->luascript) return; json_foreach(j, fullo) { snprintf(hookname, sizeof(hookname), "hooklet_%s", j->key); lua_getglobal(ld->L, hookname); if (lua_type(ld->L, -1) == LUA_TFUNCTION) { do_hook(hookname, ud, topic, fullo); } } } /* * Invoked from putrec() in storage. If the Lua putrec function is available * and it returns non-zero, do not putrec. */ int hooks_norec(struct udata *ud, char *user, char *device, char *payload) { struct luadata *ld = ud->luadata; int rc; if (ld == NULL || !ld->script) return (0); lua_settop(ld->L, 0); lua_getglobal(ld->L, "otr_putrec"); if (lua_type(ld->L, -1) != LUA_TFUNCTION) { return (0); } lua_pushstring(ld->L, user); lua_pushstring(ld->L, device); lua_pushstring(ld->L, payload); /* Invoke Lua function with our args */ if (lua_pcall(ld->L, 3, 1, 0)) { olog(LOG_ERR, "Failed to run putrec in Lua: %s", lua_tostring(ld->L, -1)); exit(1); } rc = (int)lua_tonumber(ld->L, -1); return (rc); } /* * Invoked in http to populate response FIXME */ JsonNode *hooks_http(struct udata *ud, char *user, char *device, char *payload) { struct luadata *ld = ud->luadata; char *_type = "unknown"; JsonNode *obj = NULL, *j, *fullo; debug(ud, "in hooks_http()"); if (ld == NULL || !ld->script) return (0); fullo = json_decode(payload); lua_settop(ld->L, 0); lua_getglobal(ld->L, "otr_httpobject"); if (lua_type(ld->L, -1) != LUA_TFUNCTION) { debug(ud, "no otr_httpobject function in Lua file: returning"); return (0); } lua_pushstring(ld->L, user); /* arg1 */ lua_pushstring(ld->L, device); /* arg2 */ if ((j = json_find_member(fullo, "_type")) != NULL) { if (j->tag == JSON_STRING) _type = j->string_; } lua_pushstring(ld->L, _type); /* arg3: record type */ lua_newtable(ld->L); /* arg4: table */ json_foreach(j, fullo) { if (j->tag >= JSON_ARRAY) continue; lua_pushstring(ld->L, j->key); /* table key */ if (j->tag == JSON_STRING) { lua_pushstring(ld->L, j->string_); } else if (j->tag == JSON_NUMBER) { lua_pushnumber(ld->L, j->number_); } else if (j->tag == JSON_NULL) { lua_pushnil(ld->L); } else if (j->tag == JSON_BOOL) { lua_pushboolean(ld->L, j->bool_); } lua_rawset(ld->L, -3); } // lua_pushstring(ld->L, payload); /* Invoke Lua function with our args */ /* return value is a TABLE; all else is ignored */ if (lua_pcall(ld->L, 4, 1, 0)) { olog(LOG_ERR, "Failed to run hooks_http in Lua: %s", lua_tostring(ld->L, -1)); exit(1); } /* Verify we have a table and create a JSON object from it. */ if (lua_istable(ld->L, -1)) { obj = json_mkobject(); int t = -2; lua_pushnil(ld->L); /* first key */ while (lua_next(ld->L, t) != 0) { const char *key, *val; size_t len; int type, bf, nil; lua_Number d; key = lua_tolstring(ld->L, -2, &len); type = lua_type(ld->L, -1); // printf("%s len=%zd vtype=%d\n", key, len, type); switch (type) { case LUA_TNUMBER: d = lua_tonumber(ld->L, -1); json_append_member(obj, key, json_mknumber(d)); break; case LUA_TSTRING: val = lua_tostring(ld->L, -1); json_append_member(obj, key, json_mkstring(val)); break; case LUA_TNIL: nil = lua_isnil(ld->L, -1); if (nil) json_append_member(obj, key, json_mknull()); break; case LUA_TBOOLEAN: bf = lua_toboolean(ld->L, -1); json_append_member(obj, key, json_mkbool(bf)); break; default: /* unsupported */ break; } lua_pop(ld->L, 1); } } lua_settop(ld->L, 0); return (obj); } void hooks_hook(struct udata *ud, char *topic, JsonNode *fullo) { do_hook("otr_hook", ud, topic, fullo); hooks_hooklet(ud, topic, fullo); } /* * This hook is invoked through fences.c when we determine that a movement * into or out of a geofence has caused a transition. * json is the original JSON we received enhanced with stuff from recorder. */ void hooks_transition(struct udata *ud, char *user, char *device, int event, char *desc, double wplat, double wplon, double lat, double lon, char *topic, JsonNode *json) { JsonNode *j; if ((j = json_find_member(json, "_type")) != NULL) { json_remove_from_parent(j); } json_append_member(json, "_type", json_mkstring("transition")); json_append_member(json, "event", event == ENTER ? json_mkstring("enter") : json_mkstring("leave")); json_append_member(json, "desc", json_mkstring(desc)); json_append_member(json, "wplat", json_mknumber(wplat)); json_append_member(json, "wplon", json_mknumber(wplon)); olog(LOG_DEBUG, "**** Lua hook for %s %s\n", event == ENTER ? "ENTER" : "LEAVE", desc); do_hook("otr_transition", ud, topic, json); } /* * --- Here come the functions we provide to Lua scripts. */ static int otr_log(lua_State *lua) { const char *str; if (lua_gettop(lua) >= 1) { str = lua_tostring(lua, 1); olog(LOG_INFO, "%s", str); lua_pop(lua, 1); } return 0; } /* * otr.strftime(format, seconds) * Perform a strtime(3) for Lua with the specified format and * seconds, and return the string result to Lua. As a special * case, if `seconds' is negative, use current time. */ static int otr_strftime(lua_State *lua) { const char *fmt; long secs; struct tm *tm; char buf[BUFSIZ]; if (lua_gettop(lua) >= 1) { fmt = lua_tostring(lua, 1); if ((secs = lua_tonumber(lua, 2)) < 1) secs = time(0); if ((tm = gmtime(&secs)) != NULL) { strftime(buf, sizeof(buf), fmt, tm); lua_pushstring(lua, buf); return (1); } } return (0); } /* * Requires two string arguments: key, value * These are written into the named LMDB database * called `luadb'. */ static int otr_putdb(lua_State *lua) { const char *key, *value; int rc = 0; if (lua_gettop(lua) >= 1) { key = lua_tostring(lua, 1); value = lua_tostring(lua, 2); rc = gcache_put(LuaDB, (char *)key, (char *)value); // olog(LOG_DEBUG, "LUA_PUT (%s, %s) == %d\n", key, value, rc); } return (rc); } static int otr_getdb(lua_State *lua) { char buf[BUFSIZ]; const char *key; int rc = 0, blen; if (lua_gettop(lua) >= 1) { key = lua_tostring(lua, 1); blen = gcache_get(LuaDB, (char *)key, buf, sizeof(buf)); if (blen < 0) memset(buf, 0, sizeof(buf)); // printf("K=[%s], blen=%d\n", key, blen); lua_pushstring(lua, buf); rc = 1; } return (rc); } #ifdef WITH_MQTT /* * Requires two string arguments: topic, payload * and two numeric args: qos and retain * Will be published via MQTT to the Recorder's * open connection. */ int otr_publish(lua_State *lua) { const char *topic, *payload; int qos = 0, retain = 0; int rc = 0; if (lua_gettop(lua) >= 1) { topic = lua_tostring(lua, 1); payload = lua_tostring(lua, 2); qos = lua_tonumber(lua, 3); retain = lua_tonumber(lua, 4); if (MQTTconn == NULL) { olog(LOG_WARNING, "otr_publish(%s, %s, %d, %d): NULL MQTT connection\n", topic, payload, qos, retain); } else { rc = mosquitto_publish(MQTTconn, NULL, topic, strlen(payload), payload, qos, retain); olog(LOG_DEBUG, "otr_publish(%s, %s, %d, %d) == %d\n", topic, payload, qos, retain, rc); } } return (rc); } #endif #endif /* WITH_LUA */