From 60aa73126edc770a5eb65eb3cb9ba377b1806c0d Mon Sep 17 00:00:00 2001 From: lemoer Date: Wed, 11 Aug 2021 02:23:08 +0200 Subject: [PATCH 01/14] Initial tests --- package/gluon-config-api/Makefile | 16 ++ .../config-api/controller/controller.lua | 48 ++++++ .../gluon/config-api/controller/schema.lua | 137 ++++++++++++++++++ .../gluon/config-api/parts/contact-info.lua | 34 +++++ .../lib/gluon/config-api/www/cgi-bin/api | 8 + .../luasrc/lib/gluon/upgrade/500-config-api | 36 +++++ package/libucl/Makefile | 65 +++++++++ 7 files changed, 344 insertions(+) create mode 100644 package/gluon-config-api/Makefile create mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua create mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua create mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua create mode 100755 package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api create mode 100755 package/gluon-config-api/luasrc/lib/gluon/upgrade/500-config-api create mode 100644 package/libucl/Makefile diff --git a/package/gluon-config-api/Makefile b/package/gluon-config-api/Makefile new file mode 100644 index 0000000000..cc50a0dbb5 --- /dev/null +++ b/package/gluon-config-api/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 +PKG_VERSION:=1 + +include ../gluon.mk + +define Package/gluon-config-api + TITLE:=Provides a REST API to configure the gluon node + DEPENDS:=+gluon-web +uhttpd +libucl +endef + +$(eval $(call BuildPackageGluon,gluon-config-api)) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua new file mode 100644 index 0000000000..28a3f80f2e --- /dev/null +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua @@ -0,0 +1,48 @@ +local json = require 'jsonc' +local site = require 'gluon.site' +local util = require 'gluon.util' +local ubus = require 'ubus' +local os = require 'os' +local glob = require 'posix.glob' +local libgen = require 'posix.libgen' +local simpleuci = require 'simple-uci' +local schema = require 'schema' + +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 + +local parts = load_parts() + + +entry({"config"}, call(function(http, renderer) + + http:write(json.stringify(config_get(parts), true)) + http:close() +end)) + +entry({"schema"}, call(function(http, renderer) + local total_schema = {} + for _, part in pairs(parts) do + total_schema = schema.merge_schemas(total_schema, part.schema()) + end + http:write(json.stringify(total_schema, true)) + http:close() +end)) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua new file mode 100644 index 0000000000..1f2a5e935a --- /dev/null +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua @@ -0,0 +1,137 @@ + +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 merged_keys(table1, table2) + local keys = {} + if table1 then + for k, _ in pairs(table1) do + table.insert(k) + end + end + if table2 then + for k, _ in pairs(table2) do + if not util.contains(k) then + table.insert(keys, k) + end + end + end + return keys +end + +local function merged_values(table1, table2) + local values = {} + if table1 then + for _, v in pairs(table1) do + table.insert(v) + end + end + if table2 then + for _, v in pairs(table2) do + if not util.contains(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(merged_keys(properties1, properties2)) do + local pdef1 = properties1[pkey] + local pdef2 = properties2[pkey] + + if pdef1 and pdef2 then + add_property(pkey, 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 = merge_schemas( + schema1.additionalProperties, schema2.additionalProperties) + else + merged.additionalProperties = false + end + + -- generate merged.required + merged.required = merged_values(schema1, schema2) + if #merged.required == 0 then + merged.required = nil + end + end + + -- TODO: implement array + + -- generate merged.default + merged.default = schema2.default or schema1.default +end + +return M diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua new file mode 100644 index 0000000000..d0fb997391 --- /dev/null +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua @@ -0,0 +1,34 @@ + +local M = {} + +function M.schema(site, platform) + return { + type = 'object', + properties = { + wizard = { + type = 'object', + properties = { + contact = { + type = 'string' + } + } + } + } + } +end + +function M.save(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/luasrc/lib/gluon/config-api/www/cgi-bin/api b/package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api new file mode 100755 index 0000000000..a8881b2866 --- /dev/null +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/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/luasrc/lib/gluon/upgrade/500-config-api b/package/gluon-config-api/luasrc/lib/gluon/upgrade/500-config-api new file mode 100755 index 0000000000..035de7708e --- /dev/null +++ b/package/gluon-config-api/luasrc/lib/gluon/upgrade/500-config-api @@ -0,0 +1,36 @@ +#!/usr/bin/lua + +local uci = require('simple-uci').cursor() +local site = require 'gluon.site' + +local function get_mem_total() + for line in io.lines('/proc/meminfo') do + local match = line:match('^MemTotal:%s+(%d+)') + if match then + return tonumber(match) + end + end +end + +local max_requests = 32 +if get_mem_total() < 48*1024 then + max_requests = 16 +end + +uci:section('uhttpd', 'uhttpd', 'config_api', { + listen_http = { '0.0.0.0:83', '[::]:83' }, + listen_https = {}, + + home = '/lib/gluon/config-api/www', + max_requests = max_requests, + max_connections = 100, + redirect_https = true, + rfc1918_filter = true, + cgi_prefix = '/cgi-bin', + script_timeout = 60, + network_timeout = 30, + http_keepalive = 20, + tcp_keepalive = true, +}) +uci:save('uhttpd') +uci:save('firewall') diff --git a/package/libucl/Makefile b/package/libucl/Makefile new file mode 100644 index 0000000000..db0930342d --- /dev/null +++ b/package/libucl/Makefile @@ -0,0 +1,65 @@ +# +# Copyright (C) 2007-2014 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 +# PKG_FIXUP:=patch-libtool +# PKG_FIXUP:=gettext-version + +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 + +# MAKE_FLAGS += \ +# CFLAGS="$(TARGET_CFLAGS) $(FPIC)" + +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 + + +# define Build/InstallDev +# $(INSTALL_DIR) $(1)/usr/include +# $(CP) $(PKG_BUILD_DIR)/argp.h \ +# $(1)/usr/include/ +# $(INSTALL_DIR) $(1)/usr/lib +# $(CP) $(PKG_BUILD_DIR)/libargp.a \ +# $(1)/usr/lib/ +# endef + +$(eval $(call BuildPackage,libucl)) From 64ed50c44ab8e7413d465e77c41bed9fc86b1679 Mon Sep 17 00:00:00 2001 From: lemoer Date: Wed, 11 Aug 2021 14:54:07 +0200 Subject: [PATCH 02/14] gluon-config-api: First working version Fix bugs, ... --- .../config-api/controller/controller.lua | 96 +++++++++++++++++-- .../gluon/config-api/controller/schema.lua | 16 ++-- .../gluon/config-api/parts/contact-info.lua | 5 +- .../gluon/config-api/parts/geo-location.lua | 74 ++++++++++++++ 4 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/parts/geo-location.lua diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua index 28a3f80f2e..52ec7f0ea2 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua @@ -6,7 +6,8 @@ local os = require 'os' local glob = require 'posix.glob' local libgen = require 'posix.libgen' local simpleuci = require 'simple-uci' -local schema = require 'schema' +local schema = dofile('../controller/schema.lua') -- pwd is www/ +local ucl = require "ucl" package 'gluon-config-api' @@ -29,20 +30,97 @@ function config_get(parts) 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 +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 parts = load_parts() +entry({"v1", "config"}, call(function(http, renderer) + if http.request.env.REQUEST_METHOD == 'GET' then + http:write(json.stringify(config_get(parts), true)) + elseif http.request.env.REQUEST_METHOD == 'POST' then + 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 config = json.parse(request_body) + if not config then + http:status(400, 'Bad Request') + http:write('{ "status": 400, "error": "Bad JSON in Body" }\n') + http:close() + return + end + + -- Verify schema + + local parser = ucl.parser() + local res, err = parser:parse_string(request_body) + + if not res then + http:status(500, 'Internal Server Error.') + http:write('{ "status": 500, "error": "Internal UCL Parsing Failed. This should not happen at all." }\n') + http:close() + return + end + + res, err = parser:validate(schema_get(parts)) + if not res then + http:status(400, 'Bad Request') + http:write('{ "status": 400, "error": "Schema mismatch" }\n') + http:close() + return + end -entry({"config"}, call(function(http, renderer) + -- Apply config + + config_set(parts, config) + + http:write(json.stringify(res, true)) + else + http:status(400, 'Bad Request') + http:write('Not implemented') + end - http:write(json.stringify(config_get(parts), true)) http:close() end)) -entry({"schema"}, call(function(http, renderer) - local total_schema = {} - for _, part in pairs(parts) do - total_schema = schema.merge_schemas(total_schema, part.schema()) - end - http:write(json.stringify(total_schema, true)) +entry({"v1", "schema"}, call(function(http, renderer) + http:write(json.stringify(schema_get(parts), true)) http:close() end)) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua index 1f2a5e935a..848b4b1f4a 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua @@ -34,12 +34,12 @@ local function merged_keys(table1, table2) local keys = {} if table1 then for k, _ in pairs(table1) do - table.insert(k) + table.insert(keys, k) end end if table2 then for k, _ in pairs(table2) do - if not util.contains(k) then + if not util.contains(keys, k) then table.insert(keys, k) end end @@ -51,12 +51,12 @@ local function merged_values(table1, table2) local values = {} if table1 then for _, v in pairs(table1) do - table.insert(v) + table.insert(values, v) end end if table2 then for _, v in pairs(table2) do - if not util.contains(v) then + if not util.contains(values, v) then table.insert(values, v) end end @@ -105,7 +105,7 @@ function M.merge_schemas(schema1, schema2) local pdef2 = properties2[pkey] if pdef1 and pdef2 then - add_property(pkey, merge_schemas(pdef1, pdef2)) + add_property(pkey, M.merge_schemas(pdef1, pdef2)) elseif pdef1 then add_property(pkey, deepcopy(pdef1)) elseif pdef2 then @@ -115,14 +115,14 @@ function M.merge_schemas(schema1, schema2) -- generate merged.additionalProperties if schema1.additionalProperties and schema2.additionalProperties then - merged.additionalProperties = merge_schemas( + merged.additionalProperties = M.merge_schemas( schema1.additionalProperties, schema2.additionalProperties) else merged.additionalProperties = false end -- generate merged.required - merged.required = merged_values(schema1, schema2) + merged.required = merged_values(schema1.required, schema2.required) if #merged.required == 0 then merged.required = nil end @@ -132,6 +132,8 @@ function M.merge_schemas(schema1, schema2) -- generate merged.default merged.default = schema2.default or schema1.default + + return merged end return M diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua index d0fb997391..4fb83396fc 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua @@ -13,11 +13,12 @@ function M.schema(site, platform) } } } - } + }, + required = { 'wizard' } } end -function M.save(config, uci) +function M.set(config, uci) local owner = uci:get_first("gluon-node-info", "owner") uci:set("gluon-node-info", owner, "contact", config.wizard.contact) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/geo-location.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/geo-location.lua new file mode 100644 index 0000000000..f4794fdef7 --- /dev/null +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/geo-location.lua @@ -0,0 +1,74 @@ + +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 { + type = 'object', + properties = { + wizard = { + type = 'object', + properties = { + location = { + type = 'object', + properties = { + share_location = { type = 'boolean' }, + lat = { type = 'number' }, + lon = { type = 'number' }, + altitude = altitude + }, + required = { 'lat', 'lon', 'share_location' } + } + } + } + }, + required = { 'wizard' } + } +end + +function M.set(config, uci) + local location = uci:get_first("gluon-node-info", "location") + local config_location = config.wizard.location + + if config_location then + uci:set("gluon-node-info", location, "share_location", config_location.share_location) + uci:set("gluon-node-info", location, "latitude", config_location.lat) + uci:set("gluon-node-info", location, "longitude", config_location.lon) + if config_location.altitude then -- altitude is optional + uci:set("gluon-node-info", location, "altitude", config_location.altitude) -- TODO: check if the "if" is necessary + else + uci:delete("gluon-node-info", location, "altitude") + end + else + uci:set("gluon-node-info", location, "share_location", false) + uci:delete("gluon-node-info", location, "latitude") + uci:delete("gluon-node-info", location, "longitude") + uci:delete("gluon-node-info", location, "altitude") + end + + 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 From 7ae6a2b7d0b6f3eccf1cf83047c330369d9f429a Mon Sep 17 00:00:00 2001 From: lemoer Date: Wed, 11 Aug 2021 15:36:55 +0200 Subject: [PATCH 03/14] gluon-config-api: use http method OPTIONS --- .../config-api/controller/controller.lua | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua index 52ec7f0ea2..8cabaec005 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua @@ -66,6 +66,7 @@ local parts = load_parts() entry({"v1", "config"}, call(function(http, renderer) if http.request.env.REQUEST_METHOD == 'GET' then + http:header('Content-Type', 'application/json; charset=utf-8') http:write(json.stringify(config_get(parts), true)) elseif http.request.env.REQUEST_METHOD == 'POST' then local request_body = "" @@ -82,6 +83,7 @@ entry({"v1", "config"}, call(function(http, renderer) local config = json.parse(request_body) if not config then http:status(400, 'Bad Request') + http:header('Content-Type', 'application/json; charset=utf-8') http:write('{ "status": 400, "error": "Bad JSON in Body" }\n') http:close() return @@ -94,6 +96,7 @@ entry({"v1", "config"}, call(function(http, renderer) if not res then http:status(500, 'Internal Server Error.') + http:header('Content-Type', 'application/json; charset=utf-8') http:write('{ "status": 500, "error": "Internal UCL Parsing Failed. This should not happen at all." }\n') http:close() return @@ -102,25 +105,30 @@ entry({"v1", "config"}, call(function(http, renderer) res, err = parser:validate(schema_get(parts)) if not res then http:status(400, 'Bad Request') + http:header('Content-Type', 'application/json; charset=utf-8') http:write('{ "status": 400, "error": "Schema mismatch" }\n') http:close() return end -- Apply config - config_set(parts, config) + -- Write result + http:write(json.stringify(res, true)) + elseif http.request.env.REQUEST_METHOD == 'OPTIONS' then + local result = json.stringify({ schema = schema_get(parts)}, true) + + -- Content-Length is needed, as the transfer encoding is not chunked. + http:header('Content-Length', tostring(#result)) + http:header('Content-Type', 'application/json; charset=utf-8') + http:write(result) else http:status(400, 'Bad Request') + http:header('Content-Length', '0') http:write('Not implemented') end http:close() end)) - -entry({"v1", "schema"}, call(function(http, renderer) - http:write(json.stringify(schema_get(parts), true)) - http:close() -end)) From c19e083fb497ced29bbc88ad38da32f759b226ae Mon Sep 17 00:00:00 2001 From: lemoer Date: Wed, 11 Aug 2021 15:44:50 +0200 Subject: [PATCH 04/14] gluon-config-api: "Not Implemented" is 501 --- .../luasrc/lib/gluon/config-api/controller/controller.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua index 8cabaec005..97d4f335d0 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua @@ -125,9 +125,9 @@ entry({"v1", "config"}, call(function(http, renderer) http:header('Content-Type', 'application/json; charset=utf-8') http:write(result) else - http:status(400, 'Bad Request') + http:status(501, 'Not Implemented') http:header('Content-Length', '0') - http:write('Not implemented') + http:write('Not Implemented\n') end http:close() From 493fe33effc265ea4846d9daba13f5cf8c7bf465 Mon Sep 17 00:00:00 2001 From: lemoer Date: Wed, 11 Aug 2021 15:45:20 +0200 Subject: [PATCH 05/14] gluon-config-api: add allowed_methods to OPTIONS --- .../luasrc/lib/gluon/config-api/controller/controller.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua index 97d4f335d0..3309f80018 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua @@ -118,9 +118,12 @@ entry({"v1", "config"}, call(function(http, renderer) http:write(json.stringify(res, true)) elseif http.request.env.REQUEST_METHOD == 'OPTIONS' then - local result = json.stringify({ schema = schema_get(parts)}, true) + local result = json.stringify({ + schema = schema_get(parts), + allowed_methods = {'GET', 'POST', 'OPTIONS'} + }, true) - -- Content-Length is needed, as the transfer encoding is not chunked. + -- Content-Length is needed, as the transfer encoding is not chunked for OPTIONS. http:header('Content-Length', tostring(#result)) http:header('Content-Type', 'application/json; charset=utf-8') http:write(result) From 299d6b8aa72d05ae7af19637c335dcf98d53846c Mon Sep 17 00:00:00 2001 From: lemoer Date: Wed, 11 Aug 2021 16:44:13 +0200 Subject: [PATCH 06/14] gluon-config-api: use config-mode uhttpd instance As of now, we prefer to use the existing uhttpd instance from the config-mode instead of creating our own. --- package/gluon-config-api/Makefile | 2 +- .../config-api/controller/controller.lua | 2 +- .../www/cgi-bin/api | 0 .../luasrc/lib/gluon/upgrade/500-config-api | 36 ------------------- 4 files changed, 2 insertions(+), 38 deletions(-) rename package/gluon-config-api/luasrc/lib/gluon/{config-api => config-mode}/www/cgi-bin/api (100%) delete mode 100755 package/gluon-config-api/luasrc/lib/gluon/upgrade/500-config-api diff --git a/package/gluon-config-api/Makefile b/package/gluon-config-api/Makefile index cc50a0dbb5..7ee110d830 100644 --- a/package/gluon-config-api/Makefile +++ b/package/gluon-config-api/Makefile @@ -10,7 +10,7 @@ include ../gluon.mk define Package/gluon-config-api TITLE:=Provides a REST API to configure the gluon node - DEPENDS:=+gluon-web +uhttpd +libucl + DEPENDS:=+gluon-web +uhttpd +libucl +gluon-config-mode-core endef $(eval $(call BuildPackageGluon,gluon-config-api)) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua index 3309f80018..6bb81aedab 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua @@ -6,7 +6,7 @@ local os = require 'os' local glob = require 'posix.glob' local libgen = require 'posix.libgen' local simpleuci = require 'simple-uci' -local schema = dofile('../controller/schema.lua') -- pwd is www/ +local schema = dofile('/lib/gluon/config-api/controller/schema.lua') local ucl = require "ucl" package 'gluon-config-api' diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api b/package/gluon-config-api/luasrc/lib/gluon/config-mode/www/cgi-bin/api similarity index 100% rename from package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api rename to package/gluon-config-api/luasrc/lib/gluon/config-mode/www/cgi-bin/api diff --git a/package/gluon-config-api/luasrc/lib/gluon/upgrade/500-config-api b/package/gluon-config-api/luasrc/lib/gluon/upgrade/500-config-api deleted file mode 100755 index 035de7708e..0000000000 --- a/package/gluon-config-api/luasrc/lib/gluon/upgrade/500-config-api +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/lua - -local uci = require('simple-uci').cursor() -local site = require 'gluon.site' - -local function get_mem_total() - for line in io.lines('/proc/meminfo') do - local match = line:match('^MemTotal:%s+(%d+)') - if match then - return tonumber(match) - end - end -end - -local max_requests = 32 -if get_mem_total() < 48*1024 then - max_requests = 16 -end - -uci:section('uhttpd', 'uhttpd', 'config_api', { - listen_http = { '0.0.0.0:83', '[::]:83' }, - listen_https = {}, - - home = '/lib/gluon/config-api/www', - max_requests = max_requests, - max_connections = 100, - redirect_https = true, - rfc1918_filter = true, - cgi_prefix = '/cgi-bin', - script_timeout = 60, - network_timeout = 30, - http_keepalive = 20, - tcp_keepalive = true, -}) -uci:save('uhttpd') -uci:save('firewall') From 3ded15df2973a2bef7502441b2b218718a6992a8 Mon Sep 17 00:00:00 2001 From: lemoer Date: Thu, 12 Aug 2021 01:14:51 +0200 Subject: [PATCH 07/14] gluon-config-api: simplify merged_keys() -> keys() --- .../gluon/config-api/controller/schema.lua | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua index 848b4b1f4a..f64d891f9a 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua @@ -30,24 +30,17 @@ local function merge_types(Ta, Tb) end end -local function merged_keys(table1, table2) +local function keys(tab) local keys = {} - if table1 then - for k, _ in pairs(table1) do + if tab then + for k, _ in pairs(tab) do table.insert(keys, k) end end - if table2 then - for k, _ in pairs(table2) do - if not util.contains(keys, k) then - table.insert(keys, k) - end - end - end return keys end -local function merged_values(table1, table2) +local function merge_array(table1, table2) local values = {} if table1 then for _, v in pairs(table1) do @@ -84,7 +77,6 @@ local function deepcopy(o, seen) return no end - function M.merge_schemas(schema1, schema2) local merged = {} @@ -100,7 +92,7 @@ function M.merge_schemas(schema1, schema2) local properties1 = schema1.properties or {} local properties2 = schema2.properties or {} - for _, pkey in pairs(merged_keys(properties1, properties2)) do + for _, pkey in pairs(merge_array(keys(properties1), keys(properties2))) do local pdef1 = properties1[pkey] local pdef2 = properties2[pkey] @@ -122,7 +114,7 @@ function M.merge_schemas(schema1, schema2) end -- generate merged.required - merged.required = merged_values(schema1.required, schema2.required) + merged.required = merge_array(schema1.required, schema2.required) if #merged.required == 0 then merged.required = nil end From 8d5d7531a892d029f84005ffbc44a6d31f710514 Mon Sep 17 00:00:00 2001 From: lemoer Date: Thu, 12 Aug 2021 01:41:37 +0200 Subject: [PATCH 08/14] gluon-config-api*: split to packages --- package/features | 5 + .../gluon-config-api-contact-info/Makefile | 16 ++ .../gluon/config-api/parts/contact-info.lua | 5 +- .../Makefile | 6 +- .../config-api/controller/controller.lua | 137 ++++++++++++++++++ .../gluon/config-api/controller/schema.lua | 131 +++++++++++++++++ .../lib/gluon/config-api/parts/wizard.lua | 24 +++ .../lib/gluon/config-api}/www/cgi-bin/api | 0 .../lib/gluon/config-mode/www/cgi-bin/api | 8 + .../gluon-config-api-geo-location/Makefile | 16 ++ .../gluon/config-api/parts/geo-location.lua | 3 - .../lib/gluon/config-api/parts/wizard.lua | 24 +++ .../lib/gluon/config-api/www/cgi-bin/api | 8 + 13 files changed, 373 insertions(+), 10 deletions(-) create mode 100644 package/gluon-config-api-contact-info/Makefile rename package/{gluon-config-api => gluon-config-api-contact-info}/luasrc/lib/gluon/config-api/parts/contact-info.lua (88%) rename package/{gluon-config-api => gluon-config-api-core}/Makefile (71%) create mode 100644 package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/controller.lua create mode 100644 package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/schema.lua create mode 100644 package/gluon-config-api-core/luasrc/lib/gluon/config-api/parts/wizard.lua rename package/{gluon-config-api/luasrc/lib/gluon/config-mode => gluon-config-api-core/luasrc/lib/gluon/config-api}/www/cgi-bin/api (100%) mode change 100755 => 100644 create mode 100755 package/gluon-config-api-core/luasrc/lib/gluon/config-mode/www/cgi-bin/api create mode 100644 package/gluon-config-api-geo-location/Makefile rename package/{gluon-config-api => gluon-config-api-geo-location}/luasrc/lib/gluon/config-api/parts/geo-location.lua (96%) create mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua create mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api 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/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 similarity index 88% rename from package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua rename to package/gluon-config-api-contact-info/luasrc/lib/gluon/config-api/parts/contact-info.lua index 4fb83396fc..7a7fb7df2e 100644 --- a/package/gluon-config-api/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 @@ -3,18 +3,15 @@ local M = {} function M.schema(site, platform) return { - type = 'object', properties = { wizard = { - type = 'object', properties = { contact = { type = 'string' } } } - }, - required = { 'wizard' } + } } end diff --git a/package/gluon-config-api/Makefile b/package/gluon-config-api-core/Makefile similarity index 71% rename from package/gluon-config-api/Makefile rename to package/gluon-config-api-core/Makefile index 7ee110d830..3eeacf1a1f 100644 --- a/package/gluon-config-api/Makefile +++ b/package/gluon-config-api-core/Makefile @@ -3,14 +3,14 @@ include $(TOPDIR)/rules.mk -PKG_NAME:=gluon-config-api +PKG_NAME:=gluon-config-api-core PKG_VERSION:=1 include ../gluon.mk -define Package/gluon-config-api +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)) +$(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..6bb81aedab --- /dev/null +++ b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/controller.lua @@ -0,0 +1,137 @@ +local json = require 'jsonc' +local site = require 'gluon.site' +local util = require 'gluon.util' +local ubus = require 'ubus' +local os = require 'os' +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 +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 parts = load_parts() + +entry({"v1", "config"}, call(function(http, renderer) + if http.request.env.REQUEST_METHOD == 'GET' then + http:header('Content-Type', 'application/json; charset=utf-8') + http:write(json.stringify(config_get(parts), true)) + elseif http.request.env.REQUEST_METHOD == 'POST' then + 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 config = json.parse(request_body) + if not config then + http:status(400, 'Bad Request') + http:header('Content-Type', 'application/json; charset=utf-8') + http:write('{ "status": 400, "error": "Bad JSON in Body" }\n') + http:close() + return + end + + -- Verify schema + + local parser = ucl.parser() + local res, err = parser:parse_string(request_body) + + if not res then + http:status(500, 'Internal Server Error.') + http:header('Content-Type', 'application/json; charset=utf-8') + http:write('{ "status": 500, "error": "Internal UCL Parsing Failed. This should not happen at all." }\n') + http:close() + return + end + + res, err = parser:validate(schema_get(parts)) + if not res then + http:status(400, 'Bad Request') + http:header('Content-Type', 'application/json; charset=utf-8') + http:write('{ "status": 400, "error": "Schema mismatch" }\n') + http:close() + return + end + + -- Apply config + config_set(parts, config) + + -- Write result + + http:write(json.stringify(res, true)) + elseif http.request.env.REQUEST_METHOD == 'OPTIONS' then + local result = json.stringify({ + schema = schema_get(parts), + allowed_methods = {'GET', 'POST', 'OPTIONS'} + }, true) + + -- Content-Length is needed, as the transfer encoding is not chunked for OPTIONS. + http:header('Content-Length', tostring(#result)) + http:header('Content-Type', 'application/json; charset=utf-8') + http:write(result) + 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/luasrc/lib/gluon/config-mode/www/cgi-bin/api b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/www/cgi-bin/api old mode 100755 new mode 100644 similarity index 100% rename from package/gluon-config-api/luasrc/lib/gluon/config-mode/www/cgi-bin/api rename to package/gluon-config-api-core/luasrc/lib/gluon/config-api/www/cgi-bin/api 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/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 similarity index 96% rename from package/gluon-config-api/luasrc/lib/gluon/config-api/parts/geo-location.lua rename to package/gluon-config-api-geo-location/luasrc/lib/gluon/config-api/parts/geo-location.lua index f4794fdef7..3ff5e97d06 100644 --- a/package/gluon-config-api/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 @@ -9,10 +9,8 @@ function M.schema(site, platform) end return { - type = 'object', properties = { wizard = { - type = 'object', properties = { location = { type = 'object', @@ -27,7 +25,6 @@ function M.schema(site, platform) } } }, - required = { 'wizard' } } end diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua new file mode 100644 index 0000000000..08b1343a85 --- /dev/null +++ b/package/gluon-config-api/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/luasrc/lib/gluon/config-api/www/cgi-bin/api b/package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api new file mode 100644 index 0000000000..a8881b2866 --- /dev/null +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/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 +} From 2e13eadabe3d89d0693974353649eca3901f4813 Mon Sep 17 00:00:00 2001 From: lemoer Date: Thu, 12 Aug 2021 02:24:22 +0200 Subject: [PATCH 09/14] gluon-config-api: cleanup dupplicate files --- .../config-api/controller/controller.lua | 137 ------------------ .../gluon/config-api/controller/schema.lua | 131 ----------------- .../lib/gluon/config-api/parts/wizard.lua | 24 --- .../lib/gluon/config-api/www/cgi-bin/api | 8 - 4 files changed, 300 deletions(-) delete mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua delete mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua delete mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua delete mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua deleted file mode 100644 index 6bb81aedab..0000000000 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/controller.lua +++ /dev/null @@ -1,137 +0,0 @@ -local json = require 'jsonc' -local site = require 'gluon.site' -local util = require 'gluon.util' -local ubus = require 'ubus' -local os = require 'os' -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 -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 parts = load_parts() - -entry({"v1", "config"}, call(function(http, renderer) - if http.request.env.REQUEST_METHOD == 'GET' then - http:header('Content-Type', 'application/json; charset=utf-8') - http:write(json.stringify(config_get(parts), true)) - elseif http.request.env.REQUEST_METHOD == 'POST' then - 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 config = json.parse(request_body) - if not config then - http:status(400, 'Bad Request') - http:header('Content-Type', 'application/json; charset=utf-8') - http:write('{ "status": 400, "error": "Bad JSON in Body" }\n') - http:close() - return - end - - -- Verify schema - - local parser = ucl.parser() - local res, err = parser:parse_string(request_body) - - if not res then - http:status(500, 'Internal Server Error.') - http:header('Content-Type', 'application/json; charset=utf-8') - http:write('{ "status": 500, "error": "Internal UCL Parsing Failed. This should not happen at all." }\n') - http:close() - return - end - - res, err = parser:validate(schema_get(parts)) - if not res then - http:status(400, 'Bad Request') - http:header('Content-Type', 'application/json; charset=utf-8') - http:write('{ "status": 400, "error": "Schema mismatch" }\n') - http:close() - return - end - - -- Apply config - config_set(parts, config) - - -- Write result - - http:write(json.stringify(res, true)) - elseif http.request.env.REQUEST_METHOD == 'OPTIONS' then - local result = json.stringify({ - schema = schema_get(parts), - allowed_methods = {'GET', 'POST', 'OPTIONS'} - }, true) - - -- Content-Length is needed, as the transfer encoding is not chunked for OPTIONS. - http:header('Content-Length', tostring(#result)) - http:header('Content-Type', 'application/json; charset=utf-8') - http:write(result) - 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/luasrc/lib/gluon/config-api/controller/schema.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua deleted file mode 100644 index f64d891f9a..0000000000 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/controller/schema.lua +++ /dev/null @@ -1,131 +0,0 @@ - -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/luasrc/lib/gluon/config-api/parts/wizard.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua deleted file mode 100644 index 08b1343a85..0000000000 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua +++ /dev/null @@ -1,24 +0,0 @@ - -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/luasrc/lib/gluon/config-api/www/cgi-bin/api b/package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api deleted file mode 100644 index a8881b2866..0000000000 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api +++ /dev/null @@ -1,8 +0,0 @@ -#!/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 -} From b9174390b28325a2ce475d0d569386f262d960a0 Mon Sep 17 00:00:00 2001 From: lemoer Date: Thu, 12 Aug 2021 02:25:24 +0200 Subject: [PATCH 10/14] gluon-config-api-core: cleanup and simplify --- .../config-api/controller/controller.lua | 99 ++++++++++--------- 1 file changed, 50 insertions(+), 49 deletions(-) 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 index 6bb81aedab..76d46f93eb 100644 --- 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 @@ -1,8 +1,5 @@ local json = require 'jsonc' local site = require 'gluon.site' -local util = require 'gluon.util' -local ubus = require 'ubus' -local os = require 'os' local glob = require 'posix.glob' local libgen = require 'posix.libgen' local simpleuci = require 'simple-uci' @@ -62,51 +59,61 @@ local function pump(src, snk) end end -local parts = load_parts() +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) -entry({"v1", "config"}, call(function(http, renderer) - if http.request.env.REQUEST_METHOD == 'GET' then - http:header('Content-Type', 'application/json; charset=utf-8') - http:write(json.stringify(config_get(parts), true)) - elseif http.request.env.REQUEST_METHOD == 'POST' then - 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. - -- 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) - local config = json.parse(request_body) - if not config then - http:status(400, 'Bad Request') - http:header('Content-Type', 'application/json; charset=utf-8') - http:write('{ "status": 400, "error": "Bad JSON in Body" }\n') - http:close() - return - end + if not data then + http:status(400, 'Bad Request') + json_response(http, { status = 400, error = "Bad JSON in Body" }) + http:close() + return + end - -- Verify schema + return data +end - local parser = ucl.parser() - local res, err = parser:parse_string(request_body) +local function verify_schema(schema, config) + local parser = ucl.parser() + local res, err = parser:parse_string(json.stringify(config)) - if not res then - http:status(500, 'Internal Server Error.') - http:header('Content-Type', 'application/json; charset=utf-8') - http:write('{ "status": 500, "error": "Internal UCL Parsing Failed. This should not happen at all." }\n') - http:close() - return - end + assert(res, "Internal UCL Parsing Failed. This should not happen at all.") + + res, err = parser:validate(schema) + return res +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 + +entry({"v1", "config"}, call(function(http, renderer) + local parts = load_parts() - res, err = parser:validate(schema_get(parts)) - if not res then + 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') - http:header('Content-Type', 'application/json; charset=utf-8') - http:write('{ "status": 400, "error": "Schema mismatch" }\n') + json_response(http, { status = 400, error = "Schema mismatch" }) http:close() return end @@ -115,18 +122,12 @@ entry({"v1", "config"}, call(function(http, renderer) config_set(parts, config) -- Write result - - http:write(json.stringify(res, true)) + json_response(http, { status = 200, error = "Accepted" }) elseif http.request.env.REQUEST_METHOD == 'OPTIONS' then - local result = json.stringify({ + json_response(http, { schema = schema_get(parts), allowed_methods = {'GET', 'POST', 'OPTIONS'} - }, true) - - -- Content-Length is needed, as the transfer encoding is not chunked for OPTIONS. - http:header('Content-Length', tostring(#result)) - http:header('Content-Type', 'application/json; charset=utf-8') - http:write(result) + }) else http:status(501, 'Not Implemented') http:header('Content-Length', '0') From 2d2c71f11e4bc45531b787edc6adf8da8a73ab42 Mon Sep 17 00:00:00 2001 From: lemoer Date: Thu, 12 Aug 2021 02:29:30 +0200 Subject: [PATCH 11/14] gluon-config-api*: cleanup --- .../luasrc/lib/gluon/config-api/www/cgi-bin/api | 8 -------- package/libucl/Makefile | 16 +--------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 package/gluon-config-api-core/luasrc/lib/gluon/config-api/www/cgi-bin/api diff --git a/package/gluon-config-api-core/luasrc/lib/gluon/config-api/www/cgi-bin/api b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/www/cgi-bin/api deleted file mode 100644 index a8881b2866..0000000000 --- a/package/gluon-config-api-core/luasrc/lib/gluon/config-api/www/cgi-bin/api +++ /dev/null @@ -1,8 +0,0 @@ -#!/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/libucl/Makefile b/package/libucl/Makefile index db0930342d..1d04ebf642 100644 --- a/package/libucl/Makefile +++ b/package/libucl/Makefile @@ -1,5 +1,5 @@ # -# Copyright (C) 2007-2014 OpenWrt.org +# Copyright (C) 2021 OpenWrt.org # # This is free software, licensed under the GNU General Public License v2. # See /LICENSE for more information. @@ -19,8 +19,6 @@ PKG_MAINTAINER:=Leonardo Mörlein PKG_LICENSE:=BSD-2-Clause PKG_FIXUP:=autoreconf -# PKG_FIXUP:=patch-libtool -# PKG_FIXUP:=gettext-version include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/autotools.mk @@ -40,9 +38,6 @@ define Package/libucl/description Universal configuration library parser. endef -# MAKE_FLAGS += \ -# CFLAGS="$(TARGET_CFLAGS) $(FPIC)" - define Package/libucl/install $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_BUILD_DIR)/src/.libs/libucl.so $(1)/usr/lib/ @@ -53,13 +48,4 @@ define Package/libucl/install endef -# define Build/InstallDev -# $(INSTALL_DIR) $(1)/usr/include -# $(CP) $(PKG_BUILD_DIR)/argp.h \ -# $(1)/usr/include/ -# $(INSTALL_DIR) $(1)/usr/lib -# $(CP) $(PKG_BUILD_DIR)/libargp.a \ -# $(1)/usr/lib/ -# endef - $(eval $(call BuildPackage,libucl)) From ea3cfbe8acb8b45fce843a760429f44f969f8314 Mon Sep 17 00:00:00 2001 From: lemoer Date: Thu, 12 Aug 2021 02:40:29 +0200 Subject: [PATCH 12/14] gluon-config-api-core: fixup function order --- .../gluon/config-api/controller/controller.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 index 76d46f93eb..9c44153d35 100644 --- 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 @@ -59,6 +59,15 @@ local function pump(src, snk) 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) @@ -93,15 +102,6 @@ local function verify_schema(schema, config) return res 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 - entry({"v1", "config"}, call(function(http, renderer) local parts = load_parts() From 71ee67ec182690406a7983fcdd1df6f36011cd77 Mon Sep 17 00:00:00 2001 From: lemoer Date: Thu, 12 Aug 2021 03:05:04 +0200 Subject: [PATCH 13/14] gluon-config-api-geo-location: simplify set --- .../gluon/config-api/parts/geo-location.lua | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) 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 index 3ff5e97d06..811d725bd6 100644 --- 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 @@ -30,23 +30,13 @@ end function M.set(config, uci) local location = uci:get_first("gluon-node-info", "location") - local config_location = config.wizard.location + local config_location = config.wizard.location or {} - if config_location then - uci:set("gluon-node-info", location, "share_location", config_location.share_location) - uci:set("gluon-node-info", location, "latitude", config_location.lat) - uci:set("gluon-node-info", location, "longitude", config_location.lon) - if config_location.altitude then -- altitude is optional - uci:set("gluon-node-info", location, "altitude", config_location.altitude) -- TODO: check if the "if" is necessary - else - uci:delete("gluon-node-info", location, "altitude") - end - else - uci:set("gluon-node-info", location, "share_location", false) - uci:delete("gluon-node-info", location, "latitude") - uci:delete("gluon-node-info", location, "longitude") - uci:delete("gluon-node-info", location, "altitude") - end + 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 From 3f739d513c39887052f87e9da24209833e8bf12e Mon Sep 17 00:00:00 2001 From: lemoer Date: Thu, 12 Aug 2021 03:09:36 +0200 Subject: [PATCH 14/14] gluon-config-api-core: commit all configs after set --- .../luasrc/lib/gluon/config-api/controller/controller.lua | 4 ++++ 1 file changed, 4 insertions(+) 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 index 9c44153d35..aa647cbf73 100644 --- 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 @@ -1,3 +1,4 @@ +local os = require 'os' local json = require 'jsonc' local site = require 'gluon.site' local glob = require 'posix.glob' @@ -41,6 +42,9 @@ function config_set(parts, config) 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)