luci-app-openvpn: add ovpn upload support & more 2235/head
authorDirk Brenken <dev@brenken.org>
Sat, 20 Oct 2018 19:22:49 +0000 (21:22 +0200)
committerDirk Brenken <dev@brenken.org>
Tue, 23 Oct 2018 19:17:22 +0000 (21:17 +0200)
* add the ability to upload ovpn files directly,
  incl. appropriate uci entry in openvpn config
* add the ability to edit ovpn files directly ('file' mode),
  beside the 'basic' and 'advanced' modes for normal setups
* client side checks to validate instance name & template selection,
  incl. online error reporting
* automatically remove non-ascii characters & windows line endings
  from transfered ovpn file
* change from after_commit to after_apply hook
* remove misleading default values for Port & Protocol in Overview

Signed-off-by: Dirk Brenken <dev@brenken.org>
applications/luci-app-openvpn/luasrc/controller/openvpn.lua
applications/luci-app-openvpn/luasrc/model/cbi/openvpn-basic.lua
applications/luci-app-openvpn/luasrc/model/cbi/openvpn-file.lua [new file with mode: 0644]
applications/luci-app-openvpn/luasrc/model/cbi/openvpn.lua
applications/luci-app-openvpn/luasrc/view/openvpn/cbi-select-input-add.htm
applications/luci-app-openvpn/luasrc/view/openvpn/ovpn_css.htm [new file with mode: 0644]
applications/luci-app-openvpn/luasrc/view/openvpn/pageswitch.htm

index 2e48a469a1c88b4209a108d5bf66b95152fb540e..2c153c4cd57279884a0bafedca2c73f4a9015201 100644 (file)
@@ -8,4 +8,47 @@ function index()
        entry( {"admin", "services", "openvpn"}, cbi("openvpn"), _("OpenVPN") )
        entry( {"admin", "services", "openvpn", "basic"},    cbi("openvpn-basic"),    nil ).leaf = true
        entry( {"admin", "services", "openvpn", "advanced"}, cbi("openvpn-advanced"), nil ).leaf = true
+       entry( {"admin", "services", "openvpn", "file"},     form("openvpn-file"),    nil ).leaf = true
+       entry( {"admin", "services", "openvpn", "upload"},   call("ovpn_upload"))
+end
+
+function ovpn_upload()
+       local fs     = require("nixio.fs")
+       local http   = require("luci.http")
+       local util   = require("luci.util")
+       local uci    = require("luci.model.uci").cursor()
+       local upload = http.formvalue("ovpn_file")
+       local name   = util.shellquote(http.formvalue("instance_name2"))
+       local file   = "/etc/openvpn/" ..name.. ".ovpn"
+
+       if name and upload then
+               local fp
+
+               http.setfilehandler(
+                       function(meta, chunk, eof)
+                               local data = util.trim(chunk:gsub("\r\n", "\n")) .. "\n"
+                               data = util.trim(data:gsub("[\128-\255]", ""))
+
+                               if not fp and meta and meta.name == "ovpn_file" then
+                                       fp = io.open(file, "w")
+                               end
+                               if fp and data then
+                                       fp:write(data)
+                               end
+                               if fp and eof then
+                                       fp:close()
+                               end
+                       end
+               )
+
+               if fs.access(file) then
+                       if not uci:get_first("openvpn", name) then
+                               uci:set("openvpn", name, "openvpn")
+                               uci:set("openvpn", name, "config", file)
+                               uci:save("openvpn")
+                               uci:commit("openvpn")
+                       end
+               end
+       end
+       http.redirect(luci.dispatcher.build_url('admin/services/openvpn'))
 end
index 483860c8e93ec8ef866cc99df3a0add9fc82b221..6b6323e078878abc411de7a222a7d05aac2dc59d 100644 (file)
@@ -85,4 +85,3 @@ for _, option in ipairs(basicParams) do
 end
 
 return m
-
diff --git a/applications/luci-app-openvpn/luasrc/model/cbi/openvpn-file.lua b/applications/luci-app-openvpn/luasrc/model/cbi/openvpn-file.lua
new file mode 100644 (file)
index 0000000..6878275
--- /dev/null
@@ -0,0 +1,61 @@
+-- Licensed to the public under the Apache License 2.0.
+
+local ip       = require("luci.ip")
+local fs       = require("nixio.fs")
+local util     = require("luci.util")
+local uci      = require("luci.model.uci").cursor()
+local cfg_file = uci:get("openvpn", arg[1], "config")
+
+local m = Map("openvpn")
+
+local p = m:section( SimpleSection )
+p.template = "openvpn/pageswitch"
+p.mode     = "file"
+p.instance = arg[1]
+
+if not cfg_file or not fs.access(cfg_file) then
+       local f = SimpleForm("error", nil, translatef("The OVPN config file (%s) could not be found, please check your configuration.", cfg_file or "n/a"))
+       f:append(Template("openvpn/ovpn_css"))
+       f.reset = false
+       f.submit = false
+       return m, f
+end
+
+if fs.stat(cfg_file).size >= 102400 then
+       f = SimpleForm("error", nil,
+               translatef("The size of the OVPN config file (%s) is too large for online editing in LuCI (&ge; 100 KB). ", cfg_file)
+               .. translate("Please edit this file directly in a terminal session."))
+       f:append(Template("openvpn/ovpn_css"))
+       f.reset = false
+       f.submit = false
+       return m, f
+end
+
+f = SimpleForm("cfg", nil)
+f:append(Template("openvpn/ovpn_css"))
+f.submit = translate("Save")
+f.reset = false
+
+s = f:section(SimpleSection, nil, translatef("This form allows you to modify the content of the OVPN config file (%s). ", cfg_file))
+file = s:option(TextValue, "data")
+file.datatype = "string"
+file.rows = 20
+file.rmempty = true
+
+function file.cfgvalue()
+       return fs.readfile(cfg_file) or ""
+end
+
+function file.write(self, section, data)
+       return fs.writefile(cfg_file, "\n" .. util.trim(data:gsub("\r\n", "\n")) .. "\n")
+end
+
+function file.remove(self, section, value)
+       return fs.writefile(cfg_file, "")
+end
+
+function s.handle(self, state, data)
+       return true
+end
+
+return m, f
index e17aa4085bf28341a824301a5bfa823bcb9e21ef..8f4859c0e582149386774f838ef4021dd85615c3 100644 (file)
@@ -4,7 +4,7 @@
 local fs  = require "nixio.fs"
 local sys = require "luci.sys"
 local uci = require "luci.model.uci".cursor()
-local testfullps = luci.sys.exec("ps --help 2>&1 | grep BusyBox") --check which ps do we have
+local testfullps = sys.exec("ps --help 2>&1 | grep BusyBox") --check which ps do we have
 local psstring = (string.len(testfullps)>0) and  "ps w" or  "ps axfw" --set command we use to get pid
 
 local m = Map("openvpn", translate("OpenVPN"))
@@ -13,9 +13,16 @@ s.template = "cbi/tblsection"
 s.template_addremove = "openvpn/cbi-select-input-add"
 s.addremove = true
 s.add_select_options = { }
-s.extedit = luci.dispatcher.build_url(
-       "admin", "services", "openvpn", "basic", "%s"
-)
+
+file_cfg = s:option(DummyValue, "config")
+function file_cfg.cfgvalue(self, section)
+       local file_cfg = self.map:get(section, "config")
+       if file_cfg then
+               s.extedit = luci.dispatcher.build_url("admin", "services", "openvpn", "file", "%s")
+       else
+               s.extedit = luci.dispatcher.build_url("admin", "services", "openvpn", "basic", "%s")
+       end
+end
 
 uci:load("openvpn_recipes")
 uci:foreach( "openvpn_recipes", "openvpn_recipe",
@@ -61,10 +68,10 @@ function s.create(self, name)
                if s then
                        local options = uci:get_all("openvpn_recipes", recipe)
                        for k, v in pairs(options) do
-                               uci:set("openvpn", name, k, v)
+                               if k ~= "_role" and k ~= "_description" then
+                                       uci:set("openvpn", name, k, v)
+                               end
                        end
-                       uci:delete("openvpn", name, "_role")
-                       uci:delete("openvpn", name, "_description")
                        uci:save("openvpn")
                        luci.http.redirect( self.extedit:format(name) )
                end
@@ -75,7 +82,6 @@ function s.create(self, name)
        return 0
 end
 
-
 s:option( Flag, "enabled", translate("Enabled") )
 
 local active = s:option( DummyValue, "_active", translate("Started") )
@@ -106,28 +112,27 @@ function updown.cfgvalue(self, section)
 end
 function updown.write(self, section, value)
        if self.option == "stop" then
-               luci.sys.call("/etc/init.d/openvpn stop %s" % section)
+               sys.call("/etc/init.d/openvpn stop %s" % section)
        else
-               luci.sys.call("/etc/init.d/openvpn start %s" % section)
+               sys.call("/etc/init.d/openvpn start %s" % section)
        end
        luci.http.redirect( self.redirect )
 end
 
-
 local port = s:option( DummyValue, "port", translate("Port") )
 function port.cfgvalue(self, section)
        local val = AbstractValue.cfgvalue(self, section)
-       return val or "1194"
+       return val or "-"
 end
 
 local proto = s:option( DummyValue, "proto", translate("Protocol") )
 function proto.cfgvalue(self, section)
        local val = AbstractValue.cfgvalue(self, section)
-       return val or "udp"
+       return val or "-"
 end
 
-function m.on_after_commit(self,map)
-       require("luci.sys").call('/etc/init.d/openvpn reload')
+function m.on_after_apply(self,map)
+       sys.call('/etc/init.d/openvpn reload')
 end
 
 return m
index 0166de778e4c3d984dfd994a0c8f40a4a7b01b88..09da2eb22df799f9c423e673af0c7495c3decf8a 100644 (file)
-<div class="cbi-section-create">
-       <% if self.invalid_cts then -%><div class="cbi-section-error"><% end %>
-       <input type="text" class="cbi-section-create-name" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.text" />
-       <select class="cbi-section-create-name" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.select">
-       <%- for k, v in luci.util.kspairs(self.add_select_options) do %>
-               <option value="<%=k%>"><%=luci.util.pcdata(v)%></option>
-       <% end -%>
-       </select>
-       <input class="cbi-button cbi-button-add" type="submit" value="<%:Add%>" title="<%:Add%>" />
-       <% if self.invalid_cts then %><br /><%:Invalid%></div><% end %>
+
+<script type="text/javascript">
+//<![CDATA[
+       function vpn_add()
+       {
+               var vpn_name     = div_add.querySelector("#instance_name1").value.replace(/[^\x00-\x7F]|[\s!@#$%^&*()+=\[\]{};':"\\|,<>\/?]/g,'');
+               var vpn_template = div_add.querySelector("#instance_template").value;
+               var form         = document.getElementsByName('cbi')[0];
+
+               if (!vpn_name || !vpn_name.length)
+               {
+                       return info_message(vpn_output, "<%=pcdata(translate("The 'Name' field must not be empty!"))%>", 2000);
+               }
+
+               document.getElementById("instance_name1").value = vpn_name;
+               if (document.getElementById("cbi-openvpn-" + vpn_name) != null)
+               {
+                       return info_message(vpn_output, "<%=pcdata(translate("Instance with that name already exists!"))%>", 2000);
+               }
+
+               if (!vpn_template || !vpn_template.length)
+               {
+                       return info_message(vpn_output, "<%=pcdata(translate("Please select a valid VPN template!"))%>", 2000);
+               }
+
+               if (form)
+               {
+                       form.submit();
+               }
+       }
+
+       function vpn_upload()
+       {
+               var vpn_name = div_upload.querySelector("#instance_name2").value.replace(/[^\x00-\x7F]|[\s!@#$%^&*()+=\[\]{};':"\\|,<>\/?]/g,'');
+               var vpn_file = document.getElementById("ovpn_file").value;
+               var form     = document.getElementsByName('cbi')[0];
+
+               if (!vpn_name || !vpn_name.length)
+               {
+                       return info_message(vpn_output, "<%=pcdata(translate("The 'Name' field must not be empty!"))%>", 2000);
+               }
+
+               document.getElementById("instance_name2").value = vpn_name;
+               if (document.getElementById("cbi-openvpn-" + vpn_name) != null)
+               {
+                       return info_message(vpn_output, "<%=pcdata(translate("Instance with that name already exists!"))%>", 2000);
+               }
+
+               if (!vpn_file || !vpn_file.length)
+               {
+                       return info_message(vpn_output, "<%=pcdata(translate("Please select a valid OVPN config file to upload!"))%>", 2000);
+               }
+
+               if (form)
+               {
+                       form.enctype = 'multipart/form-data';
+                       form.action  = '<%=url('admin/services/openvpn/upload')%>';
+                       form.submit();
+               }
+       }
+
+       function info_message(output, msg, timeout)
+       {
+               timeout = timeout || 0;
+               output.innerHTML = '<em>' + msg + '</em>';
+               if (timeout > 0)
+               {
+                       setTimeout(function(){ output.innerHTML=""}, timeout);
+               }
+       }
+//]]>
+</script>
+
+<%+openvpn/ovpn_css%>
+
+<div class="cbi-section-node">
+       <div class="table cbi-section-table">
+               <h4><%:Template based configuration%></h4>
+               <div class="tr cbi-section-table-row" id="div_add">
+                       <div class="td">
+                               <input type="text" maxlength="20" placeholder="Instance name" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.text" id="instance_name1" />
+                       </div>
+                       <div class="td">
+                               <select id="instance_template" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.select">
+                                       <option value="" selected="selected" disabled="disabled"><%:Select template ...%></option>
+                                       <%- for k, v in luci.util.kspairs(self.add_select_options) do %>
+                                               <option value="<%=k%>"><%=luci.util.pcdata(v)%></option>
+                                       <% end -%>
+                               </select>
+                       </div>
+                       <div class="td">
+                               <input class="cbi-button cbi-button-add" type="submit" onclick="vpn_add(); return false;" value="<%:Add%>" title="<%:Add template based configuration%>" /><br />
+                       </div>
+               </div>
+               <h4><%:OVPN configuration file upload%></h4>
+               <div class="tr cbi-section-table-row" id="div_upload">
+                       <div class="td">
+                               <input type="text" maxlength="20" placeholder="Instance name" name="instance_name2" id="instance_name2" />
+                       </div>
+                       <div class="td">
+                               <input type="file" name="ovpn_file" id="ovpn_file" accept="application/x-openvpn-profile,.ovpn" />
+                       </div>
+                       <div class="td">
+                               <input class="cbi-button cbi-button-add" type="submit" onclick="vpn_upload(); return false;" value="<%:Upload%>" title="<%:Upload ovpn file%>" />
+                       </div>
+               </div>
+       </div>
+       <div class="vpn-output">
+               <span id="vpn_output"></span>
+       </div>
 </div>
diff --git a/applications/luci-app-openvpn/luasrc/view/openvpn/ovpn_css.htm b/applications/luci-app-openvpn/luasrc/view/openvpn/ovpn_css.htm
new file mode 100644 (file)
index 0000000..c7062b8
--- /dev/null
@@ -0,0 +1,44 @@
+<style type="text/css">
+       h4
+       {
+               white-space: nowrap;
+               border-bottom: 0px;
+               margin: 10px 5px 5px 5px;
+       }
+       .tr
+       {
+               border: 0px;
+               text-align: left;
+       }
+       .td
+       {
+               text-align: left;
+               border-top: 0px;
+               margin: 5px;
+       }
+       .vpn-output
+       {
+               box-shadow: none;
+               margin: 10px 5px 5px 5px;
+               color: #a22;
+       }
+       textarea
+       {
+               border: 1px solid #cccccc;
+               padding: 5px;
+               font-size: 12px;
+               font-family: monospace;
+               resize: none;
+               white-space: pre;
+               overflow-wrap: normal;
+               overflow-x: scroll;
+       }
+       a
+       {
+               line-height: 1.5;
+       }
+       hr
+       {
+               margin: 0.5em 0;
+       }
+</style>
index 8cb019b461046940282af3a1fbccc4b2650855dd..17beef0d39cc80b153e392776614e4135a3e9787 100644 (file)
@@ -4,25 +4,31 @@
  Licensed to the public under the Apache License 2.0.
 -%>
 
+<%+openvpn/ovpn_css%>
+
 <div class="cbi-section">
        <h3>
-               <a href="<%=url('admin/services/openvpn')%>"><%:Overview%></a> &raquo;
+               <a href="<%=url('admin/services/openvpn')%>"><%:Overview%></a> &#187;
                <%=luci.i18n.translatef("Instance \"%s\"", self.instance)%>
        </h3>
-
-       <% if self.mode == "basic" then %>
-               <a href="<%=url('admin/services/openvpn/advanced', self.instance, "Service")%>"><%:Switch to advanced configuration »%></a>
-       <% else %>
-               <a href="<%=url('admin/services/openvpn/basic', self.instance)%>"><%:« Switch to basic configuration%></a>
-               <hr style="margin:0.5em 0" />
+       <% if self.mode == "file" then %>
+               <a href="<%=url('admin/services/openvpn/basic', self.instance)%>"><%:Switch to basic configuration%> &#187;</a><p/>
+               <a href="<%=url('admin/services/openvpn/advanced', self.instance, "Service")%>"><%:Switch to advanced configuration%> &#187;</a>
+               <hr />
+       <% elseif self.mode == "basic" then %>
+               <a href="<%=url('admin/services/openvpn/advanced', self.instance, "Service")%>"><%:Switch to advanced configuration%> &#187;</a><p/>
+               <a href="<%=url('admin/services/openvpn/file', self.instance)%>"><%:Switch to file based configuration%> &#187;</a>
+               <hr />
+       <% elseif self.mode == "advanced" then %>
+               <a href="<%=url('admin/services/openvpn/basic', self.instance)%>"><%:Switch to basic configuration%> &#187;</a><p/>
+               <a href="<%=url('admin/services/openvpn/file', self.instance)%>"><%:Switch to file based configuration%> &#187;</a>
+               <hr />
                <%:Configuration category%>:
                <% for i, c in ipairs(self.categories) do %>
                        <% if c == self.category then %>
                                <strong><%=translate(c)%></strong>
                        <% else %>
-                               <a href="<%=luci.dispatcher.build_url(
-                                       "admin", "services", "openvpn", "advanced", self.instance, c
-                               )%>"><%=translate(c)%></a>
+                               <a href="<%=luci.dispatcher.build_url("admin", "services", "openvpn", "advanced", self.instance, c)%>"><%=translate(c)%></a>
                        <% end %>
                        <% if next(self.categories, i) then %>|<% end %>
                <% end %>