diff --git a/package/features b/package/features index 72887e3af0..dff1a8861a 100644 --- a/package/features +++ b/package/features @@ -31,6 +31,11 @@ when(_'web-advanced' and _'autoupdater', { 'gluon-web-autoupdater', }) +feature('config-api', { + 'gluon-config-api-contact-info', + 'gluon-config-api-geo-location', +}) + when(_'mesh-batman-adv-15', { 'gluon-ebtables-limit-arp', diff --git a/package/gluon-config-api-contact-info/Makefile b/package/gluon-config-api-contact-info/Makefile new file mode 100644 index 0000000000..419796f7a9 --- /dev/null +++ b/package/gluon-config-api-contact-info/Makefile @@ -0,0 +1,16 @@ +# Copyright (C) 2021 Leonardo Moerlein +# This is free software, licensed under the Apache 2.0 license. + +include $(TOPDIR)/rules.mk + +PKG_NAME:=gluon-config-api-contact-info +PKG_VERSION:=1 + +include ../gluon.mk + +define Package/gluon-config-api-contact-info + TITLE:=Allows the user to provide contact information to be distributed in the mesh + DEPENDS:=+gluon-config-api-core +endef + +$(eval $(call BuildPackageGluon,gluon-config-api-contact-info)) diff --git a/package/gluon-config-api-contact-info/luasrc/lib/gluon/config-api/parts/contact-info.lua b/package/gluon-config-api-contact-info/luasrc/lib/gluon/config-api/parts/contact-info.lua new file mode 100644 index 0000000000..7a7fb7df2e --- /dev/null +++ b/package/gluon-config-api-contact-info/luasrc/lib/gluon/config-api/parts/contact-info.lua @@ -0,0 +1,32 @@ + +local M = {} + +function M.schema(site, platform) + return { + properties = { + wizard = { + properties = { + contact = { + type = 'string' + } + } + } + } + } +end + +function M.set(config, uci) + local owner = uci:get_first("gluon-node-info", "owner") + + uci:set("gluon-node-info", owner, "contact", config.wizard.contact) + uci:save("gluon-node-info") +end + +function M.get(uci, config) + local owner = uci:get_first("gluon-node-info", "owner") + + config.wizard = config.wizard or {} + config.wizard.contact = uci:get("gluon-node-info", owner, "contact") +end + +return M diff --git a/package/gluon-config-api-core/Makefile b/package/gluon-config-api-core/Makefile new file mode 100644 index 0000000000..3eeacf1a1f --- /dev/null +++ b/package/gluon-config-api-core/Makefile @@ -0,0 +1,16 @@ +# Copyright (C) 2021 Leonardo Moerlein +# This is free software, licensed under the Apache 2.0 license. + +include $(TOPDIR)/rules.mk + +PKG_NAME:=gluon-config-api-core +PKG_VERSION:=1 + +include ../gluon.mk + +define Package/gluon-config-api-core + TITLE:=Provides a REST API to configure the gluon node + DEPENDS:=+gluon-web +uhttpd +libucl +gluon-config-mode-core +endef + +$(eval $(call BuildPackageGluon,gluon-config-api-core)) diff --git a/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/controller.lua b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/controller.lua new file mode 100644 index 0000000000..aa647cbf73 --- /dev/null +++ b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/controller.lua @@ -0,0 +1,142 @@ +local os = require 'os' +local json = require 'jsonc' +local site = require 'gluon.site' +local glob = require 'posix.glob' +local libgen = require 'posix.libgen' +local simpleuci = require 'simple-uci' +local schema = dofile('/lib/gluon/config-api/controller/schema.lua') +local ucl = require "ucl" + +package 'gluon-config-api' + +function load_parts() + local parts = {} + for _, f in pairs(glob.glob('/lib/gluon/config-api/parts/*.lua')) do + table.insert(parts, dofile(f)) + end + return parts +end + +function config_get(parts) + local config = {} + local uci = simpleuci.cursor() + + for _, part in pairs(parts) do + part.get(uci, config) + end + + return config +end + +function schema_get(parts) + local total_schema = {} + for _, part in pairs(parts) do + total_schema = schema.merge_schemas(total_schema, part.schema(site, nil)) + end + return total_schema +end + +function config_set(parts, config) + local uci = simpleuci.cursor() + + for _, part in pairs(parts) do + part.set(config, uci) + end + + -- commit all uci configs + os.execute('uci commit') +end + +local function pump(src, snk) + while true do + local chunk, src_err = src() + local ret, snk_err = snk(chunk, src_err) + + if not (chunk and ret) then + local err = src_err or snk_err + if err then + return nil, err + else + return true + end + end + end +end + +local function json_response(http, obj) + local result = json.stringify(obj, true) + http:header('Content-Type', 'application/json; charset=utf-8') + -- Content-Length is needed, as the transfer encoding is not chunked for + -- http method OPTIONS. + http:header('Content-Length', tostring(#result)) + http:write(result..'\n') +end + +local function get_request_body_as_json(http) + local request_body = "" + pump(http.input, function (data) + if data then + request_body = request_body .. data + end + end) + + -- Verify that we really have JSON input. UCL is able to parse other + -- config formats as well. Those config formats allow includes and so on. + -- This may be a security issue. + + local data = json.parse(request_body) + + if not data then + http:status(400, 'Bad Request') + json_response(http, { status = 400, error = "Bad JSON in Body" }) + http:close() + return + end + + return data +end + +local function verify_schema(schema, config) + local parser = ucl.parser() + local res, err = parser:parse_string(json.stringify(config)) + + assert(res, "Internal UCL Parsing Failed. This should not happen at all.") + + res, err = parser:validate(schema) + return res +end + +entry({"v1", "config"}, call(function(http, renderer) + local parts = load_parts() + + if http.request.env.REQUEST_METHOD == 'GET' then + json_response(http, config_get(parts)) + elseif http.request.env.REQUEST_METHOD == 'POST' then + local config = get_request_body_as_json(http) + + -- Verify schema + if not verify_schema(schema_get(parts), config) then + http:status(400, 'Bad Request') + json_response(http, { status = 400, error = "Schema mismatch" }) + http:close() + return + end + + -- Apply config + config_set(parts, config) + + -- Write result + json_response(http, { status = 200, error = "Accepted" }) + elseif http.request.env.REQUEST_METHOD == 'OPTIONS' then + json_response(http, { + schema = schema_get(parts), + allowed_methods = {'GET', 'POST', 'OPTIONS'} + }) + else + http:status(501, 'Not Implemented') + http:header('Content-Length', '0') + http:write('Not Implemented\n') + end + + http:close() +end)) diff --git a/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/schema.lua b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/schema.lua new file mode 100644 index 0000000000..f64d891f9a --- /dev/null +++ b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/schema.lua @@ -0,0 +1,131 @@ + +local util = require 'gluon.util' + +local M = {} + +local function merge_types(Ta, Tb) + -- T == nil means "any" type is allowed + + if not Ta then return Tb end + if not Tb then return Ta end + + -- convert scalar types to arrays + if type(Ta) ~= 'table' then Ta = { Ta } end + if type(Tb) ~= 'table' then Tb = { Tb } end + + local Tnew = {} + + for _, t in pairs(Ta) do + if util.contains(Tb, t) then + table.insert(Tnew, t) + end + end + + assert(#Tnew > 0, 'ERROR: The schema does not match anything at all.') + + if #Tnew == 1 then + return Tnew[1] -- convert to scalar + else + return Tnew + end +end + +local function keys(tab) + local keys = {} + if tab then + for k, _ in pairs(tab) do + table.insert(keys, k) + end + end + return keys +end + +local function merge_array(table1, table2) + local values = {} + if table1 then + for _, v in pairs(table1) do + table.insert(values, v) + end + end + if table2 then + for _, v in pairs(table2) do + if not util.contains(values, v) then + table.insert(values, v) + end + end + end + return values +end + +local function deepcopy(o, seen) + seen = seen or {} + if o == nil then return nil end + if seen[o] then return seen[o] end + + local no + if type(o) == 'table' then + no = {} + seen[o] = no + + for k, v in next, o, nil do + no[deepcopy(k, seen)] = deepcopy(v, seen) + end + setmetatable(no, deepcopy(getmetatable(o), seen)) + else -- number, string, boolean, etc + no = o + end + return no +end + +function M.merge_schemas(schema1, schema2) + local merged = {} + + merged.type = merge_types(schema1.type, schema2.type) + + function add_property(pkey, pdef) + merged.properties = merged.properties or {} + merged.properties[pkey] = pdef + end + + if not merged.type or merged.type == 'object' then + -- generate merged.properties + local properties1 = schema1.properties or {} + local properties2 = schema2.properties or {} + + for _, pkey in pairs(merge_array(keys(properties1), keys(properties2))) do + local pdef1 = properties1[pkey] + local pdef2 = properties2[pkey] + + if pdef1 and pdef2 then + add_property(pkey, M.merge_schemas(pdef1, pdef2)) + elseif pdef1 then + add_property(pkey, deepcopy(pdef1)) + elseif pdef2 then + add_property(pkey, deepcopy(pdef2)) + end + end + + -- generate merged.additionalProperties + if schema1.additionalProperties and schema2.additionalProperties then + merged.additionalProperties = M.merge_schemas( + schema1.additionalProperties, schema2.additionalProperties) + else + merged.additionalProperties = false + end + + -- generate merged.required + merged.required = merge_array(schema1.required, schema2.required) + if #merged.required == 0 then + merged.required = nil + end + end + + -- TODO: implement array + + -- generate merged.default + merged.default = schema2.default or schema1.default + + return merged +end + +return M diff --git a/package/gluon-config-api-core/luasrc/lib/gluon/config-api/parts/wizard.lua b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/parts/wizard.lua new file mode 100644 index 0000000000..08b1343a85 --- /dev/null +++ b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/parts/wizard.lua @@ -0,0 +1,24 @@ + +local M = {} + +function M.schema(site, platform) + return { + type = 'object', + properties = { + wizard = { + type = 'object', + additionalProperties = false + } + }, + additionalProperties = false, + required = { 'wizard' } + } +end + +function M.set(config, uci) +end + +function M.get(uci, config) +end + +return M diff --git a/package/gluon-config-api-core/luasrc/lib/gluon/config-mode/www/cgi-bin/api b/package/gluon-config-api-core/luasrc/lib/gluon/config-mode/www/cgi-bin/api new file mode 100755 index 0000000000..a8881b2866 --- /dev/null +++ b/package/gluon-config-api-core/luasrc/lib/gluon/config-mode/www/cgi-bin/api @@ -0,0 +1,8 @@ +#!/usr/bin/lua + +require 'gluon.web.cgi' { + base_path = '/lib/gluon/config-api', + + layout_package = 'gluon-config-api', + layout_template = 'theme/layout', -- only used for error pages +} diff --git a/package/gluon-config-api-geo-location/Makefile b/package/gluon-config-api-geo-location/Makefile new file mode 100644 index 0000000000..57c95e65c9 --- /dev/null +++ b/package/gluon-config-api-geo-location/Makefile @@ -0,0 +1,16 @@ +# Copyright (C) 2021 Leonardo Moerlein +# This is free software, licensed under the Apache 2.0 license. + +include $(TOPDIR)/rules.mk + +PKG_NAME:=gluon-config-api-geo-location +PKG_VERSION:=1 + +include ../gluon.mk + +define Package/gluon-config-api-geo-location + TITLE:=Set geographic location of a node + DEPENDS:=+gluon-config-api-core +endef + +$(eval $(call BuildPackageGluon,gluon-config-api-geo-location)) diff --git a/package/gluon-config-api-geo-location/luasrc/lib/gluon/config-api/parts/geo-location.lua b/package/gluon-config-api-geo-location/luasrc/lib/gluon/config-api/parts/geo-location.lua new file mode 100644 index 0000000000..811d725bd6 --- /dev/null +++ b/package/gluon-config-api-geo-location/luasrc/lib/gluon/config-api/parts/geo-location.lua @@ -0,0 +1,61 @@ + +local M = {} + +function M.schema(site, platform) + local altitude = nil + + if site.config_mode.geo_location.show_altitude(false) then + altitude = { type = 'number' } + end + + return { + properties = { + wizard = { + properties = { + location = { + type = 'object', + properties = { + share_location = { type = 'boolean' }, + lat = { type = 'number' }, + lon = { type = 'number' }, + altitude = altitude + }, + required = { 'lat', 'lon', 'share_location' } + } + } + } + }, + } +end + +function M.set(config, uci) + local location = uci:get_first("gluon-node-info", "location") + local config_location = config.wizard.location or {} + + uci:set("gluon-node-info", location, "share_location", + config_location.share_location or false) + uci:set("gluon-node-info", location, "latitude", config_location.lat) + uci:set("gluon-node-info", location, "longitude", config_location.lon) + uci:set("gluon-node-info", location, "altitude", config_location.altitude) + + uci:save("gluon-node-info") +end + +function M.get(uci, config) + config.wizard = config.wizard or {} + + local location = uci:get_first("gluon-node-info", "location") + local lon = uci:get("gluon-node-info", location, "longitude") + + if lon then + config.wizard.location = { + share_location = uci:get_bool("gluon-node-info", location, "share_location"), + lat = tonumber(uci:get("gluon-node-info", location, "latitude")), + lon = tonumber(lon), + -- if uci:get() returns nil, then altitude will not be present in the result + altitude = tonumber(uci:get("gluon-node-info", location, "altitude")) + } + end +end + +return M diff --git a/package/libucl/Makefile b/package/libucl/Makefile new file mode 100644 index 0000000000..1d04ebf642 --- /dev/null +++ b/package/libucl/Makefile @@ -0,0 +1,51 @@ +# +# Copyright (C) 2021 OpenWrt.org +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=libucl +PKG_VERSION:=0.8.1 +PKG_RELEASE:=1 + +PKG_SOURCE_PROTO:=git +PKG_SOURCE_URL:=https://github.com/vstakhov/libucl +PKG_SOURCE_VERSION:=e6c5d8079b95796099693b0889f07a036f78ad77 +PKG_MAINTAINER:=Leonardo Mörlein + +PKG_LICENSE:=BSD-2-Clause + +PKG_FIXUP:=autoreconf + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/autotools.mk + +CONFIGURE_ARGS += \ + --enable-lua + +define Package/libucl + SECTION:=libs + CATEGORY:=Libraries + TITLE:=Config Parsing + DEPENDS:=+liblua + URL:=https://github.com/vstakhov/libucl +endef + +define Package/libucl/description + Universal configuration library parser. +endef + +define Package/libucl/install + $(INSTALL_DIR) $(1)/usr/lib + $(CP) $(PKG_BUILD_DIR)/src/.libs/libucl.so $(1)/usr/lib/ + $(CP) $(PKG_BUILD_DIR)/src/.libs/libucl.so.5 $(1)/usr/lib/ + $(CP) $(PKG_BUILD_DIR)/src/.libs/libucl.so.5.1.0 $(1)/usr/lib/ + $(INSTALL_DIR) $(1)/usr/lib/lua + $(CP) $(PKG_BUILD_DIR)/lua/.libs/ucl.so $(1)/usr/lib/lua/ucl.so +endef + + +$(eval $(call BuildPackage,libucl))