diff --git a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/ServiceController.php b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/ServiceController.php index e606beaf8..2151e181c 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/ServiceController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/ServiceController.php @@ -138,19 +138,22 @@ class ServiceController extends ApiControllerBase $template = $mdlCP->getTemplateByName($templateName); } - // needs optimization, we don't want to store data in our config that's already in our standard system - // (like bootstrap code) + // cleanse input content, we only want to save changed files into our config if (strlen($this->request->getPost("content", "striptags", "")) > 20 || strlen((string)$template->content) == 0 ) { - $temp_filename = 'cp_' . (string)$mdlCP->uuid . '.tmp'; + $temp_filename = 'cp_' . (string)$template->getAttributes()['uuid'] . '.tmp'; file_put_contents('/tmp/'.$temp_filename, $this->request->getPost("content", "striptags", "")); - // strip defaults from template (standard js libs, etc) + // strip defaults and unchanged files from template (standard js libs, etc) $backend = new Backend(); $response = $backend->configdpRun("captiveportal strip_template", array($temp_filename)); - // todo, handle response - // only set data if new content is provided - $template->content = $this->request->getPost("content", "striptags", ""); + unlink($temp_filename); + $result = json_decode($response, true); + if ($result != null && !array_key_exists('error', $result)) { + $template->content = $result['payload']; + } else { + return array("name" => $templateName, "error" => $result['error']); + } } $template->name = $templateName; diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/exclude.list b/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/exclude.list new file mode 100644 index 000000000..574fb2215 --- /dev/null +++ b/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/exclude.list @@ -0,0 +1,10 @@ +# This list contains the filenames that may not be overwritten by the user when creating custom templates. +# All items in this list won't be stored in the config.xml by our framework +css/bootstrap.css.map +css/bootstrap.min.css +fonts/glyphicons-halflings-regular.eot +fonts/glyphicons-halflings-regular.svg +fonts/glyphicons-halflings-regular.ttf +fonts/glyphicons-halflings-regular.woff +fonts/glyphicons-halflings-regular.woff2 +#js/bootstrap.min.js diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/strip_template.py b/src/opnsense/scripts/OPNsense/CaptivePortal/strip_template.py new file mode 100755 index 000000000..170cf6e17 --- /dev/null +++ b/src/opnsense/scripts/OPNsense/CaptivePortal/strip_template.py @@ -0,0 +1,105 @@ +#!/usr/local/bin/python2.7 + +""" + Copyright (c) 2015 Deciso B.V. - Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + -------------------------------------------------------------------------------------- + read a user provided base64 encoded zip file and generate a new one without standard + files. (strips javascript etc) + Filename should be provided by user parameter and must be available in /tmp/ +""" +import sys +import os.path +import binascii +import StringIO +import zipfile +import ujson +import md5 + +htdocs_default_root = '/usr/local/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default' + +def load_exclude_list(): + """ load exclude list, files that should be removed from the input stream + """ + result = [] + filename = '%s/exclude.list' % htdocs_default_root + for line in open(filename, 'r').read().split('\n'): + line = line.strip() + if len(line) > 1 and line[0] != '#': + result.append(line) + return result + + +response = dict() +if len(sys.argv) < 2: + response['error'] = 'Filename parameter missing' +else: + input_filename = '/tmp/%s' % os.path.basename(sys.argv[1]) + try: + zip_content = open(input_filename,'r').read().decode('base64') + except binascii.Error: + # not in base64 + response['error'] = 'Not a base64 encoded file' + except IOError: + # file npt found + response['error'] = 'Error reading file' + +if 'error' not in response: + exclude_list = load_exclude_list() + input_data = StringIO.StringIO(zip_content) + output_data = StringIO.StringIO() + with zipfile.ZipFile(input_data, mode='r', compression=zipfile.ZIP_DEFLATED) as zf_in: + with zipfile.ZipFile(output_data, mode='w', compression=zipfile.ZIP_DEFLATED) as zf_out: + # the zip content may be in a folder, use index to track actual location + index_location = None + for zf_info in zf_in.infolist(): + if zf_info.filename.find('index.html') > -1: + if index_location is None or len(index_location) > len(zf_info.filename): + index_location = zf_info.filename + if index_location is not None: + for zf_info in zf_in.infolist(): + if zf_info.filename[-1] != '/': + filename = zf_info.filename.replace(index_location.replace('index.html',''),'') + # ignore internal osx metadata files, maybe we need to ignore some others (windows?) as well + # here. + if filename.split('/')[0] in ('__MACOSX') or filename.split('/')[-1] == '.DS_Store': + continue + if filename not in exclude_list: + file_data = zf_in.read(zf_info.filename) + src_filename = '%s/%s' % (htdocs_default_root, filename) + if os.path.isfile(src_filename): + md5_src = md5.new(open(src_filename, 'rb').read()).hexdigest() + md5_new = md5.new(file_data).hexdigest() + if md5_src != md5_new: + # changed file + zf_out.writestr(filename, file_data) + else: + # new file, always write + zf_out.writestr(filename, file_data) + if 'error' not in response: + response['payload'] = output_data.getvalue().encode('base64').strip() + response['size'] = len(response['payload']) + +print(ujson.dumps(response)) diff --git a/src/opnsense/service/conf/actions.d/actions_captiveportal.conf b/src/opnsense/service/conf/actions.d/actions_captiveportal.conf index e11b35691..72681ffe7 100644 --- a/src/opnsense/service/conf/actions.d/actions_captiveportal.conf +++ b/src/opnsense/service/conf/actions.d/actions_captiveportal.conf @@ -39,3 +39,9 @@ command:/usr/local/opnsense/scripts/OPNsense/CaptivePortal/fetch_template.py parameters: type:script_output message:fetch captiveportal web template package + +[strip_template] +command:/usr/local/opnsense/scripts/OPNsense/CaptivePortal/strip_template.py +parameters:%s +type:script_output +message:strip user captiveportal web template package and return new base64 encoded content