summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-radicale2/luasrc/model
diff options
context:
space:
mode:
authorDaniel F. Dickinson <cshored@thecshore.com>2019-01-02 06:18:40 -0500
committerDaniel F. Dickinson <cshored@thecshore.com>2019-01-10 05:52:12 -0500
commit29e01e0e5b4bf9a25e88de7309637b0f461ef6a9 (patch)
tree92343df8693a4ef380504fe2f2359d21a4eced20 /applications/luci-app-radicale2/luasrc/model
parent1893892581707fcd83e684a7a03dd6b5d3c2c3a2 (diff)
luci-app-radicale2: Add v2 of CalDAV/CardDAV server
Radicale 2.x has been out for some time, and has better support for a number of clients as well as new features, so radicale2 package has been added to packages, and this is the corresponding UI. Signed-off-by: Daniel F. Dickinson <cshored@thecshore.com>
Diffstat (limited to 'applications/luci-app-radicale2/luasrc/model')
-rw-r--r--applications/luci-app-radicale2/luasrc/model/cbi/radicale2/auth.lua209
-rw-r--r--applications/luci-app-radicale2/luasrc/model/cbi/radicale2/logging.lua40
-rw-r--r--applications/luci-app-radicale2/luasrc/model/cbi/radicale2/server.lua144
-rw-r--r--applications/luci-app-radicale2/luasrc/model/cbi/radicale2/storage.lua50
4 files changed, 443 insertions, 0 deletions
diff --git a/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/auth.lua b/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/auth.lua
new file mode 100644
index 000000000..71fd3a32e
--- /dev/null
+++ b/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/auth.lua
@@ -0,0 +1,209 @@
+-- Licensed to the public under the Apache License 2.0.
+
+local rad2 = luci.controller.radicale2
+local fs = require("nixio.fs")
+local util = require("luci.util")
+
+local m = Map("radicale2", translate("Radicale 2.x"),
+ translate("A lightweight CalDAV/CardDAV server"))
+
+local s = m:section(NamedSection, "auth", "section", translate("Authentication"))
+s.addremove = true
+s.anonymous = false
+
+local at = s:option(ListValue, "type", translate("Authentication Type"))
+at:value("", translate("Default (htpasswd file from users below)"))
+at:value("htpasswd", translate("htpasswd file (manually populated)"))
+at:value("none", translate("No authentication"))
+at:value("remote_user", translate("REMOTE_USER from web server"))
+at:value("http_x_remote_user", translate("X-Remote-User from web server"))
+at.default = ""
+at.rmempty = true
+
+local o = s:option(Value, "htpasswd_filename", translate("Filename"), translate("htpasswd-formatted file filename"))
+o:depends("type", "htpasswd")
+o.rmempty = true
+o.placeholder = "/etc/radicale2/users"
+o.default = ""
+
+local hte = s:option(ListValue, "htpasswd_encryption", translate("Encryption"), translate("Password encryption method"))
+hte:depends("type", "htpasswd")
+hte:depends("type", "")
+hte:value("plain", translate("Plaintext"))
+hte:value("sha1", translate("SHA1"))
+hte:value("ssha", translate("SSHA"))
+hte:value("crypt", translate("crypt"))
+if rad2.pymodexists("passlib") then
+ hte:value("md5", translate("md5"))
+ if rad2.pymodexists("bcrypt") then
+ hte:value("bcrypt", translate("bcrypt"))
+ end
+end
+hte.default = "plain"
+hte.rmempty = true
+
+if not rad2.pymodexists("bcrypt") then
+ o = s:option(DummyValue, "nobcrypt", translate("Insecure hashes"), translate("Install python3-passlib and python3-bcrypt to enable a secure hash"))
+else
+ o = s:option(DummyValue, "nobcrypt", translate("Insecure hashes"), translate("Select bcrypt above to enable a secure hash"))
+ o:depends("htpasswd_encrypt","")
+ o:depends("htpasswd_encrypt","plain")
+ o:depends("htpasswd_encrypt","sha1")
+ o:depends("htpasswd_encrypt","ssha")
+ o:depends("htpasswd_encrypt","crypt")
+ o:depends("htpasswd_encrypt","md5")
+end
+
+o = s:option(Value, "delay", translate("Retry Delay"), translate("Required time between a failed authentication attempt and trying again"))
+o.rmempty = true
+o.default = 1
+o.datatype = "uinteger"
+o:depends("type", "")
+o:depends("type", "htpasswd")
+o:depends("type", "remote_user")
+o:depends("type", "http_x_remote_user")
+
+s = m:section(TypedSection, "user", translate("User"), translate("Users and Passwords"))
+s.addremove = true
+s.anonymous = true
+
+o = s:option(Value, "name", translate("Username"))
+o.rmempty = true
+o.placeholder = "johndoe"
+
+if rad2.pymodexists("passlib") then
+
+local plainpass = s:option(Value, "plain_pass", translate("Plaintext Password"))
+plainpass.placeholder = "Example password"
+plainpass.password = true
+
+local ppconfirm = s:option(Value, "plain_pass_confirm", translate("Confirm Plaintext Password"))
+ppconfirm.placeholder = "Example password"
+ppconfirm.password = true
+
+plainpass.cfgvalue = function(self, section)
+ return self:formvalue(section)
+end
+
+plainpass.write = function(self, section)
+ return true
+end
+
+
+ppconfirm.cfgvalue = plainpass.cfgvalue
+ppconfirm.write = plainpass.write
+
+plainpass.validate = function(self, value, section)
+ if self:cfgvalue(section) ~= ppconfirm:cfgvalue(section) then
+ return nil, translate("Password and confirmation do not match")
+ end
+ return AbstractValue.validate(self, value, section)
+end
+
+ppconfirm.validate = function(self, value, section)
+ if self:cfgvalue(section) ~= plainpass:cfgvalue(section) then
+ return nil, translate("Password and confirmation do not match")
+ end
+ return AbstractValue.validate(self, value, section)
+end
+
+local pass = s:option(Value, "password", translate("Encrypted Password"), translate("If 'Plaintext Password' filled and matches 'Confirm Plaintext Password' then this field becomes of hash of that password, otherwise this field remains the existing hash (you can also put your own hash value for the type of hash listed above)."))
+pass.password = true
+pass.rmempty = false
+
+function encpass(self, section)
+ local plainvalue = plainpass:cfgvalue(section)
+ local pvc = ppconfirm:cfgvalue(section)
+ local encvalue, err
+
+ if not plainvalue or not pvc or plainvalue == "" or pvc == "" or plainvalue ~= pvc then
+ return nil
+ end
+ local enctype = hte:formvalue("auth")
+ if not enctype then
+ enctype = hte:cfgvalue("auth")
+ end
+ if not enctype or enctype == "" or enctype == "plain" then
+ return plainvalue
+ end
+
+ encvalue, err = util.ubus("rad2-enc", "encrypt", { type = enctype, plainpass = plainvalue })
+ if not encvalue then
+ return nil
+ end
+
+ return encvalue and encvalue.encrypted_password
+end
+
+pass.parse = function(self, section, novld)
+ local encvalue
+ if self:cfgvalue(section) then
+ if not plainpass:cfgvalue(section) then
+ return Value.parse(self, section)
+ else
+ encvalue = encpass(self, section)
+ if encvalue then
+ self.formvalue = function(self, section)
+ return encvalue
+ end
+ return Value.parse(self, section, novld)
+ else
+ self.formvalue = self.cfgvalue
+ return Value.parse(self, section, novld)
+ end
+ end
+ else
+ encvalue = encpass(self, section)
+ if encvalue then
+ self.formvalue = function(self, section)
+ return encvalue
+ end
+ return Value.parse(self, section, novld)
+ else
+ return nil
+ end
+ end
+end
+
+else
+local pass = s:option(Value, "password", translate("Encrypted Password"), translate("Generate this field using an generator for Apache htpasswd-style authentication files (for the hash format you have chosen above), or install python3-passlib to enable the ability to create the hash by entering the plaintext in a field that will appear on this page if python3-passlib is installed."))
+pass.password = true
+pass.rmempty = false
+
+end -- python3-passlib installed
+
+-- TODO: Allow configuration of rights file from this page
+local s = m:section(NamedSection, "section", "rights", translate("Rights"), translate("User-based ACL Settings"))
+s.addremove = true
+s.anonymous = false
+
+o = s:option(ListValue, "type", translate("Rights Type"))
+o:value("", translate("Default (owner only)"))
+o:value("owner_only", translate("RO: None, RW: Owner"))
+o:value("authenticated", translate("RO: None, RW: Authenticated Users"))
+o:value("owner_write", translate("RO: Authenticated Users, RW: Owner"))
+o:value("from_file", translate("Based on settings in 'Rights File'"))
+o:value("none", translate("RO: All, RW: All"))
+o.default = ""
+o.rmempty = true
+
+rights_file = s:option(FileUpload, "file", translate("Rights File"))
+rights_file.rmempty = true
+rights_file:depends("type", "from_file")
+
+o = s:option(Button, "remove_conf",
+ translate("Remove configuration for rights file"),
+ translate("This permanently deletes the rights file and configuration to use same."))
+o.inputstyle = "remove"
+o:depends("type", "from_file")
+
+function o.write(self, section)
+ if cert_file:cfgvalue(section) and fs.access(o:cfgvalue(section)) then fs.unlink(rights_file:cfgvalue(section)) end
+ self.map:del(section, "file")
+ self.map:del(section, "rights_file")
+ luci.http.redirect(luci.dispatcher.build_url("admin", "services", "radicale2", "auth"))
+end
+
+-- TODO: Allow configuration rights file from this page
+
+return m
diff --git a/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/logging.lua b/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/logging.lua
new file mode 100644
index 000000000..779bef859
--- /dev/null
+++ b/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/logging.lua
@@ -0,0 +1,40 @@
+-- Licensed to the public under the Apache License 2.0.
+
+local m = Map("radicale2", translate("Radicale 2.x"),
+ translate("A lightweight CalDAV/CardDAV server"))
+
+local s = m:section(NamedSection, "logging", "section", translate("Logging"))
+s.addremove = true
+s.anonymous = false
+
+local logging_file = nil
+
+logging_file = s:option(FileUpload, "config", translate("Logging File"), translate("Log configuration file (no file means default procd which ends up in syslog"))
+logging_file.rmempty = true
+logging_file.default = ""
+
+o = s:option(Button, "remove_conf", translate("Remove configuration for logging"),
+ translate("This permanently deletes configuration for logging"))
+o.inputstyle = "remove"
+
+function o.write(self, section)
+ if logging_file:cfgvalue(section) and fs.access(logging_file:cfgvalue(section)) then fs.unlink(loggin_file:cfgvalue(section)) end
+ self.map:del(section, "config")
+ luci.http.redirect(luci.dispatcher.build_url("admin", "services", "radicale2", "logging"))
+end
+
+o = s:option(Flag, "debug", translate("Debug"), translate("Send debug information to logs"))
+o.rmempty = true
+o.default = o.disabled
+
+o = s:option(Flag, "full_environment", translate("Dump Environment"), translate("Include full environment in logs"))
+o.rmempty = true
+o.default = o.disabled
+
+o = s:option(Flag, "mask_passwords", translate("Mask Passwords"), translate("Redact passwords in logs"))
+o.rmempty = true
+o.default = o.enabled
+
+-- TODO: Allow configuration logging file from this page
+
+return m
diff --git a/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/server.lua b/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/server.lua
new file mode 100644
index 000000000..47ef868b8
--- /dev/null
+++ b/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/server.lua
@@ -0,0 +1,144 @@
+-- Licensed to the public under the Apache License 2.0.
+
+local fs = require("nixio.fs")
+local rad2 = require "luci.controller.radicale2"
+local http = require("luci.http")
+
+local m = Map("radicale2", translate("Radicale 2.x"),
+ translate("A lightweight CalDAV/CardDAV server"))
+
+s = m:section(SimpleSection, translate("Radicale v2 Web UI"))
+s.addremove = false
+s.anonymous = true
+
+o = s:option(DummyValue, "radicale2_webui_go", translate("Go to Radicale v2 Web UI"))
+o.template = "cbi/raduigo"
+o.section = "cbi-radicale2_webui"
+
+local s = m:section(NamedSection, "server", "section", translate("Server Settings"))
+s.addremove = true
+s.anonymous = false
+
+o.section = "cbi-radicale2_web_ui"
+
+local lhttp = nil
+local certificate_file = nil
+local key_file = nil
+local certificate_authority_file = nil
+
+s:tab("general", translate("General Settings"))
+s:tab("advanced", translate("Advanced Settings"))
+
+lhttp = s:taboption("general", DynamicList, "host", translate("HTTP(S) Listeners (address:port)"))
+lhttp.datatype = "list(ipaddrport(1))"
+lhttp.placeholder = "127.0.0.1:5232"
+
+o = s:taboption("advanced", Value, "max_connection", translate("Max Connections"), translate("Maximum number of simultaneous connections"))
+o.rmempty = true
+o.placeholder = 20
+o.datatype = "uinteger"
+
+
+o = s:taboption("advanced", Value, "max_content_length", translate("Max Content Length"), translate("Maximum size of request body (bytes)"))
+o.rmempty = true
+o.datatype = "uinteger"
+o.placeholder = 100000000
+
+o = s:taboption("advanced", Value, "timeout", translate("Timeout"), translate("Socket timeout (seconds)"))
+o.rmempty = true
+o.placeholder = 30
+o.datatype = "uinteger"
+
+sslon = s:taboption("general", Flag, "ssl", translate("SSL"), translate("Enable SSL connections"))
+sslon.rmempty = true
+sslon.default = o.disabled
+sslon.formvalue = function(self, section)
+ if not rad2.pymodexists('ssl') then
+ return false
+ end
+ return Flag.formvalue(self, section)
+end
+
+cert_file = s:taboption("general", FileUpload, "certificate", translate("Certificate"))
+cert_file.rmempty = true
+cert_file:depends("ssl", true)
+
+key_file = s:taboption("general", FileUpload, "key", translate("Private Key"))
+key_file.rmempty = true
+key_file:depends("ssl", true)
+
+ca_file = s:taboption("general", FileUpload, "certificate_authority", translate("Client Certificate Authority"), translate("For verifying client certificates"))
+ca_file.rmempty = true
+ca_file:depends("ssl", true)
+
+o = s:taboption("advanced", Value, "ciphers", translate("Allowed Ciphers"), translate("See python3-openssl documentation for available ciphers"))
+o.rmempty = true
+o:depends("ssl", true)
+
+o = s:taboption("advanced", Value, "protocol", translate("Use Protocol"), translate("See python3-openssl documentation for available protocols"))
+o.rmempty = true
+o:depends("ssl", true)
+o.placeholder = "PROTOCOL_TLSv1_2"
+
+o = s:taboption("general", Button, "remove_conf",
+ translate("Remove configuration for certificate, key, and CA"),
+ translate("This permanently deletes the cert, key, and configuration to use same."))
+o.inputstyle = "remove"
+o:depends("ssl", true)
+
+function o.write(self, section)
+ if cert_file:cfgvalue(section) and fs.access(cert_file:cfgvalue(section)) then fs.unlink(cert_file:cfgvalue(section)) end
+ if key_file:cfgvalue(section) and fs.access(key_file:cfgvalue(section)) then fs.unlink(key_file:cfgvalue(section)) end
+ if ca_file:cfgvalue(section) and fs.access(key_file:cfgvalue(section)) then fs.unlink(ca_file:cfgvalue(section)) end
+ self.map:del(section, "certificate")
+ self.map:del(section, "key")
+ self.map:del(section, "certificate_authority")
+ self.map:del(section, "protocol")
+ self.map:del(section, "ciphers")
+ luci.http.redirect(luci.dispatcher.build_url("admin", "services", "radicale2", "server"))
+end
+
+if not rad2.pymodexists('ssl') then
+ o = s:taboption("general", DummyValue, "sslnotpreset", translate("SSL not available"), translate("Install package python3-openssl to support SSL connections"))
+end
+
+o = s:taboption("advanced", Flag, "dns_lookup", translate("DNS Lookup"), translate("Lookup reverse DNS for clients for logging"))
+o.rmempty = true
+o.default = o.enabled
+
+o = s:taboption("advanced", Value, "realm", translate("Realm"), translate("HTTP(S) Basic Authentication Realm"))
+o.rmempty = true
+o.placeholder = "Radicale - Password Required"
+
+local s = m:section(NamedSection, "web", "section", translate("Web UI"))
+s.addremove = true
+s.anonymous = false
+
+o = s:option(ListValue, "type", translate("Web UI Type"))
+o:value("", "Default (Built-in)")
+o:value("internal", "Built-in")
+o:value("none", "None")
+o.default = ""
+o.rmempty = true
+
+local s = m:section(NamedSection, "headers", "section", translate("Headers"), translate("HTTP(S) Headers"))
+s.addremove = true
+s.anonymous = false
+
+o = s:option(Value, "cors", translate("CORS"), translate("Header: X-Access-Control-Allow-Origin"))
+o.rmempty = true
+o.placeholder = "*"
+
+local s = m:section(NamedSection, "encoding", "section", translate("Document Encoding"))
+s.addremove = true
+s.anonymous = false
+
+o = s:option(Value, "request", translate("Request"), translate("Encoding for responding to requests/events"))
+o.rmempty = true
+o.placeholder = "utf-8"
+
+o = s:option(Value, "stock", translate("Storage"), translate("Encoding for storing local collections"))
+o.rmempty = true
+o.placeholder = "utf-8"
+
+return m
diff --git a/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/storage.lua b/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/storage.lua
new file mode 100644
index 000000000..3440296ed
--- /dev/null
+++ b/applications/luci-app-radicale2/luasrc/model/cbi/radicale2/storage.lua
@@ -0,0 +1,50 @@
+-- Licensed to the public under the Apache License 2.0.
+
+local rad2 = luci.controller.radicale2
+local fs = require("nixio.fs")
+
+local m = Map("radicale2", translate("Radicale 2.x"),
+ translate("A lightweight CalDAV/CardDAV server"))
+
+local s = m:section(NamedSection, "storage", "section", translate("Storage"))
+s.addremove = true
+s.anonymous = false
+
+o = s:option(ListValue, "type", translate("Storage Type"))
+o:value("", translate("Default (multifilesystem)"))
+o:value("multifilesystem", translate("Multiple files on filesystem"))
+o.default = ""
+o.rmempty = true
+
+o = s:option(Value, "filesystem_folder", translate("Folder"), translate("Folder in which to store collections"))
+o:depends("type", "")
+o:depends("type", "multifilesystem")
+o.rmempty = true
+o.placeholder = "/srv/radicale2/data"
+
+o = s:option(Flag, "filesystem_locking", translate("Use File Locks"), translate("Prevent other instances or processes from modifying collections while in use"))
+o:depends("type", "")
+o:depends("type", "multifilesystem")
+o.rmempty = true
+o.default = o.enabled
+
+o = s:option(Value, "max_sync_token_age", translate("Max Sync Token Age"), translate("Delete sync token that are older (seconds)"))
+o:depends("type", "")
+o:depends("type", "multifilesystem")
+o.rmempty = true
+o.placeholder = 2592000
+o.datatype = "uinteger"
+
+o = s:option(Flag, "filesystem_close_lock_file", translate("Close Lock File"), translate("Close the lock file when no more clients are waiting"))
+o:depends("type", "")
+o:depends("type", "multifilesystem")
+o.rmempty = true
+o.default = o.disabled
+
+o = s:option(Value, "hook", translate("Hook"), translate("Command that is run after changes to storage"))
+o:depends("type", "")
+o:depends("type", "multifilesystem")
+o.rmempty = true
+o.placeholder = ("Example: ([ -d .git ] || git init) && git add -A && (git diff --cached --quiet || git commit -m \"Changes by \"%(user)s")
+
+return m