diff --git a/LICENSE b/LICENSE index baf0a1005..87aa83187 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023 A. Kulikov +Copyright (c) 2023-2025 A. Kulikov Copyright (c) 2015-2025 Ad Schellevis Copyright (c) 2022 agh1467 Copyright (c) 2024 Alex Smith @@ -8,6 +8,7 @@ Copyright (c) 2021 Axelrtgs Copyright (c) 2023 Bernhard Frenking Copyright (c) 2023 Cannon Matthews Copyright (c) 2023-2025 Cedrik Pischem +Copyright (c) 2025 Christopher Linn, BackendMedia IT-Services GmbH Copyright (c) 2019 Cloudfence - Julio Camargo (JCC) Copyright (c) 2005-2006 Colin Smith Copyright (c) 2021 Dan Lundqvist @@ -19,7 +20,7 @@ Copyright (c) 2020 devNan0 Copyright (c) 2023 Dmitry Shinkaruk Copyright (c) 2024 DollarSign23 Copyright (c) 2006 Eric Friesen -Copyright (c) 2008-2010 Ermal Luçi +Copyright (c) 2008-2014 Ermal Luçi Copyright (c) 2016-2019 EURO-LOG AG Copyright (c) 2017-2020 Fabian Franz Copyright (c) 2019 Felix Matouschek @@ -41,24 +42,32 @@ Copyright (c) 2024 laraveluser Copyright (c) 2023 Liam Steckler Copyright (c) 2020-2021 Manuel Faux Copyright (c) 2021 Manuel Hofmann -Copyright (c) 2003-2004 Manuel Kasper +Copyright (c) 2003-2005 Manuel Kasper Copyright (c) 2023 Marc Bartelt Copyright (c) 2021 Marcel Koepfli Copyright (c) 2021 Markus Peter Copyright (c) 2022 Markus Reiter Copyright (c) 2020 Martin Wasley Copyright (c) 2022 Marvo2011 -Copyright (c) 2017-2024 Michael Muenz +Copyright (c) 2025 Matthias Valvekens +Copyright (c) 2025 Maxime Thiebaut +Copyright (c) 2017-2025 Michael Muenz Copyright (c) 2024 Michał Brzeziński Copyright (c) 2024 Mike Shuey Copyright (c) 2023-2024 Mikhail Kharisov Copyright (c) 2023 mleinart Copyright (c) 2024 MVZ Labor Ludwigsburg GbR +Copyright (c) 2025 Neil Merchant +Copyright (c) 2025 NetBird GmbH +Copyright (c) 2025 Nick Card Copyright (c) 2021-2024 Nicola Pellegrini Copyright (c) 2022 Nikolaj Brinch Jørgensen Copyright (c) 2021 Nim G Copyright (c) 2023 Oliver Hartl +Copyright (c) 2025 Oliver Traber Copyright (c) 2024 Olly Baker +Copyright (c) 2019 Pascal Mathis +Copyright (c) 2025 Ralph Moser, PJ Monitoring GmbH Copyright (c) 2024 realizelol Copyright (c) 2022 Robbert Rijkse Copyright (c) 2023 sattamjh @@ -66,14 +75,16 @@ Copyright (c) 2004-2012 Scott Ullrich Copyright (c) 2010 Seth Mos Copyright (c) 2024 Sheridan Computers Copyright (c) 2008 Shrew Soft Inc. -Copyright (c) 2017-2019 Smart-Soft -Copyright (c) 2013 Stanley P. Miller \ stan-qaz +Copyright (c) 2017-2018 Smart-Soft +Copyright (c) 2025 squared GmbH Copyright (c) 2020 Starkstromkonsument Copyright (c) 2023-2024 Thomas Cekal Copyright (c) 2020 Tobias Boehnert Copyright (c) 2024 txr13 Copyright (c) 2024 W516 Copyright (c) 2022 Wouter Deurholt +Copyright (c) 2025 Yann Bayart +Copyright (c) 2025 Yann Demoulin Copyright (c) 2015 YoungJoo.Kim All rights reserved. diff --git a/Makefile b/Makefile index b8928dbc9..839ba7cce 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2015-2024 Franco Fichtner +# Copyright (c) 2015-2025 Franco Fichtner # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -23,19 +23,23 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. -all: - @cat ${.CURDIR}/README.md | ${PAGER} +PLUGINSDIR?= ${.CURDIR} -.include "Mk/defaults.mk" - -CATEGORIES!= ls -1d [a-z0-9]* -CATEGORIES:= ${CATEGORIES:Nruleset.xml} +_CATEGORIES!= ls -1d [a-z0-9]* +CATEGORIES?= ${_CATEGORIES} .for CATEGORY in ${CATEGORIES} _${CATEGORY}!= ls -1d ${CATEGORY}/* PLUGIN_DIRS+= ${_${CATEGORY}} .endfor +all: + @cat ${.CURDIR}/README.md | ${PAGER} + +.include "Mk/defaults.mk" +.include "Mk/common.mk" +.include "Mk/git.mk" + list: .for PLUGIN_DIR in ${PLUGIN_DIRS} @echo ${PLUGIN_DIR} -- $$(${MAKE} -C ${PLUGIN_DIR} -v PLUGIN_COMMENT) \ @@ -44,7 +48,7 @@ list: .endfor # shared targets that are sane to run from the root directory -TARGETS= clean glint lint plist-fix revision style style-fix style-python sweep test +TARGETS= clean glint lint revision style sweep test .for TARGET in ${TARGETS} ${TARGET}: diff --git a/Mk/common.mk b/Mk/common.mk new file mode 100644 index 000000000..bfd8be9ff --- /dev/null +++ b/Mk/common.mk @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +.-include "${PLUGINSDIR}/../core/Mk/common.mk" diff --git a/Mk/contrib.mk b/Mk/contrib.mk new file mode 100644 index 000000000..22f9df38d --- /dev/null +++ b/Mk/contrib.mk @@ -0,0 +1,46 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +.for TARGET in ${PLUGIN_CONTRIB} +.if "${ROOT_${TARGET}}" == "" +.error "No ROOT directory set for target: ${TARGET}" +.endif + +# fixup root target dir +ROOT_${TARGET}:=${ROOT_${TARGET}:S/^\/$//} + +install-${TARGET}: + @mkdir -p ${DESTDIR}${ROOT_${TARGET}}/${TARGET} + @tar -C ${TARGET} -cf - . | tar -C ${DESTDIR}${ROOT_${TARGET}}/${TARGET} -xf - + +install: install-${TARGET} + +plist-${TARGET}: + @(cd ${TARGET}; find * -type f) | while read FILE; do \ + echo "${ROOT_${TARGET}}/${TARGET}/$${FILE}"; \ + done + +plist: plist-${TARGET} +.endfor diff --git a/Mk/defaults.mk b/Mk/defaults.mk index 9cf73ce3a..77a93b13a 100644 --- a/Mk/defaults.mk +++ b/Mk/defaults.mk @@ -34,6 +34,9 @@ PKG= true .endif GIT!= which git || echo true +SCRIPTSDIR= ${PLUGINSDIR}/Scripts +TEMPLATESDIR= ${PLUGINSDIR}/Templates + GITVERSION= ${SCRIPTSDIR}/version.sh _PLUGIN_ARCH!= uname -p @@ -43,11 +46,13 @@ VERSIONBIN= ${LOCALBASE}/sbin/opnsense-version .if exists(${VERSIONBIN}) _PLUGIN_ABI!= ${VERSIONBIN} -a -PLUGIN_ABI?= ${_PLUGIN_ABI} +PLUGIN_ABIS?= ${_PLUGIN_ABI} .else -PLUGIN_ABI?= 25.1 +PLUGIN_ABIS?= 25.7 .endif +PLUGIN_ABI?= ${PLUGIN_ABIS:[1]} + PLUGIN_MAINS= master main PLUGIN_MAIN?= ${PLUGIN_MAINS:[1]} PLUGIN_STABLE?= stable/${PLUGIN_ABI} @@ -66,7 +71,6 @@ _PLUGIN_PYTHON!=${PYTHONLINK} -V PLUGIN_PYTHON?= ${_PLUGIN_PYTHON:[2]:S/./ /g:[1..2]:tW:S/ //} .endif - .for REPLACEMENT in ABI PHP PYTHON . if empty(PLUGIN_${REPLACEMENT}) . warning Cannot build without PLUGIN_${REPLACEMENT} set @@ -91,78 +95,8 @@ SED_REPLACE= # empty SED_REPLACE+= -e "s=%%${REPLACEMENT}%%=${${REPLACEMENT}}=g" .endfor -ARGS= diff feed mfc - -# handle argument expansion for required targets -.for TARGET in ${.TARGETS} -_TARGET= ${TARGET:C/\-.*//} -.if ${_TARGET} != ${TARGET} -.for ARGUMENT in ${ARGS} -.if ${_TARGET} == ${ARGUMENT} -${_TARGET}_ARGS+= ${TARGET:C/^[^\-]*(\-|\$)//:S/,/ /g} -${TARGET}: ${_TARGET} -.endif -.endfor -${_TARGET}_ARG= ${${_TARGET}_ARGS:[0]} -.endif -.endfor - -ensure-stable: - @if ! git show-ref --verify --quiet refs/heads/${PLUGIN_STABLE}; then \ - git update-ref refs/heads/${PLUGIN_STABLE} refs/remotes/origin/${PLUGIN_STABLE}; \ - git config branch.${PLUGIN_STABLE}.merge refs/heads/${PLUGIN_STABLE}; \ - git config branch.${PLUGIN_STABLE}.remote origin; \ - fi - -diff_ARGS?= . - -diff: ensure-stable - @git diff --stat -p ${PLUGIN_STABLE} ${.CURDIR}/${diff_ARGS:[1]} - -feed: ensure-stable - @git log --stat -p --reverse ${PLUGIN_STABLE}...${feed_ARGS:[1]}~1 - -mfc_ARGS?= . - -mfc: ensure-stable -.for MFC in ${mfc_ARGS} -.if exists(${MFC}) - @git diff --stat -p ${PLUGIN_STABLE} ${.CURDIR}/${MFC} > /tmp/mfc.diff - @git checkout ${PLUGIN_STABLE} - @git apply /tmp/mfc.diff - @git add ${.CURDIR}/${MFC} - @if ! git diff --quiet HEAD; then \ - git commit -m "${MFC:S/^.$/${PLUGIN_DIR}/}: sync with ${PLUGIN_MAIN}"; \ - fi -.else - @git checkout ${PLUGIN_STABLE} - @if ! git cherry-pick -x ${MFC}; then \ - git cherry-pick --abort; \ - fi -.endif - @git checkout ${PLUGIN_MAIN} -.endfor - -stable: - @git checkout ${PLUGIN_STABLE} - -${PLUGIN_MAINS}: - @git checkout ${PLUGIN_MAIN} - -rebase: - @git checkout ${PLUGIN_STABLE} - @git rebase -i - @git checkout ${PLUGIN_MAIN} - -log: - @git log --stat -p ${PLUGIN_STABLE} - -push: - @git checkout ${PLUGIN_STABLE} - @git push - @git checkout ${PLUGIN_MAIN} - -reset: - @git checkout ${PLUGIN_STABLE} - @git reset --hard HEAD~1 - @git checkout ${PLUGIN_MAIN} +WRKDIR?= ${.CURDIR}/work +MFCDIR?= /tmp/mfc.dir +PKGDIR?= ${WRKDIR}/pkg +WRKSRC?= ${WRKDIR}/src +TESTDIR?= ${.CURDIR}/src/opnsense/mvc/tests diff --git a/Mk/devel.mk b/Mk/devel.mk new file mode 100644 index 000000000..f1a8bfd79 --- /dev/null +++ b/Mk/devel.mk @@ -0,0 +1,28 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +# This file merely exists on the development +# branch to force plugins to development mode: +PLUGIN_DEVEL?= yes diff --git a/Mk/git.mk b/Mk/git.mk new file mode 100644 index 000000000..4aded1e18 --- /dev/null +++ b/Mk/git.mk @@ -0,0 +1,32 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +CORE_ABI= ${PLUGIN_ABI} +CORE_ABIS= ${PLUGIN_ABIS} +CORE_MAIN= ${PLUGIN_MAIN} +CORE_MAINS= ${PLUGIN_MAINS} +CORE_STABLE= ${PLUGIN_STABLE} + +.-include "${PLUGINSDIR}/../core/Mk/git.mk" diff --git a/Mk/lint.mk b/Mk/lint.mk new file mode 100644 index 000000000..ddc3c2ad7 --- /dev/null +++ b/Mk/lint.mk @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +.-include "${PLUGINSDIR}/../core/Mk/lint.mk" diff --git a/Mk/plugins.mk b/Mk/plugins.mk index 33d5be910..3144a7285 100644 --- a/Mk/plugins.mk +++ b/Mk/plugins.mk @@ -1,4 +1,4 @@ -# Copyright (c) 2015-2024 Franco Fichtner +# Copyright (c) 2015-2025 Franco Fichtner # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -23,14 +23,13 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. +PLUGINSDIR?= ${.CURDIR}/../.. + all: check + @cat ${.CURDIR}/${PLUGIN_DESC} | ${PAGER} .include "defaults.mk" -PLUGINSDIR?= ${.CURDIR}/../.. -SCRIPTSDIR= ${PLUGINSDIR}/Scripts -TEMPLATESDIR= ${PLUGINSDIR}/Templates - .if exists(${GIT}) && exists(${GITVERSION}) PLUGIN_COMMIT!= ${GITVERSION} .else @@ -51,6 +50,12 @@ PLUGIN_REVISION?= 0 PLUGIN_REQUIRES= PLUGIN_NAME PLUGIN_VERSION PLUGIN_COMMENT \ PLUGIN_MAINTAINER +.include "common.mk" +.include "git.mk" +.include "lint.mk" +.include "style.mk" +.include "sweep.mk" + check: .for PLUGIN_REQUIRE in ${PLUGIN_REQUIRES} . if "${${PLUGIN_REQUIRE}}" == "" @@ -63,7 +68,7 @@ _PLUGIN_COMMENT:= ${PLUGIN_COMMENT} .if defined(_PLUGIN_DEVEL) PLUGIN_DEVEL?:= ${_PLUGIN_DEVEL} .else -PLUGIN_DEVEL?= yes +.-include "devel.mk" .endif PLUGIN_PREFIX?= os- @@ -100,6 +105,7 @@ PLUGIN_DIR?= ${.CURDIR:S/\// /g:[-2]}/${.CURDIR:S/\// /g:[-1]} .if "${PLUGIN_DEVEL}" != "" PLUGIN_CONFLICTS+= ${PLUGIN_NAME} PLUGIN_PKGSUFFIX= ${PLUGIN_SUFFIX} +PLUGIN_TIER= 4 .else PLUGIN_CONFLICTS+= ${PLUGIN_NAME}${PLUGIN_SUFFIX} PLUGIN_PKGSUFFIX= # empty @@ -146,13 +152,26 @@ manifest: check echo "annotations $$(cat ${WRKSRC}${LOCALBASE}/opnsense/version/${PLUGIN_NAME})"; \ fi -scripts: check scripts-pre scripts-auto scripts-manual scripts-post +# Package scripts generation handling 101: +# +# "auto" generates automatic hooks that a plugin may need in order to +# reload on the fly. ".pre" and ".post" suffixed files can be used to +# extend the auto-generated content. +# +# "manual" overwrites the automatic script and also ignores ".pre" and +# ".post" files since they do not make sense in manual mode. +# +# Furthermore, variable replacement via %%PLUGIN_VAR%% takes place in +# "manual" as well as ".pre" and ".post" scripts provided. + +scripts: check scripts-pre scripts-auto scripts-post scripts-manual scripts-pre: @for SCRIPT in ${PLUGIN_SCRIPTS}; do \ rm -f ${DESTDIR}/$${SCRIPT}; \ if [ -f ${.CURDIR}/$${SCRIPT}.pre ]; then \ - cp ${.CURDIR}/$${SCRIPT}.pre ${DESTDIR}/$${SCRIPT}; \ + sed ${SED_REPLACE} -- ${.CURDIR}/$${SCRIPT}.pre > \ + ${DESTDIR}/$${SCRIPT}; \ fi; \ done @@ -203,22 +222,25 @@ scripts-auto: done \ fi -scripts-manual: - @for SCRIPT in ${PLUGIN_SCRIPTS}; do \ - if [ -f ${.CURDIR}/$${SCRIPT} ]; then \ - cp ${.CURDIR}/$${SCRIPT} ${DESTDIR}/$${SCRIPT}; \ - fi; \ - done - scripts-post: @for SCRIPT in ${PLUGIN_SCRIPTS}; do \ if [ -f ${.CURDIR}/$${SCRIPT}.post ]; then \ - cat ${.CURDIR}/$${SCRIPT}.post >> ${DESTDIR}/$${SCRIPT}; \ + sed ${SED_REPLACE} -- ${.CURDIR}/$${SCRIPT}.post >> \ + ${DESTDIR}/$${SCRIPT}; \ + fi; \ + done + +scripts-manual: + @for SCRIPT in ${PLUGIN_SCRIPTS}; do \ + if [ -f ${.CURDIR}/$${SCRIPT} ]; then \ + sed ${SED_REPLACE} -- ${.CURDIR}/$${SCRIPT} > \ + ${DESTDIR}/$${SCRIPT}; \ fi; \ done install: check @mkdir -p ${DESTDIR}${LOCALBASE}/opnsense/version + @if [ -d ${.CURDIR}/contrib ]; then ${MAKE} DESTDIR=$$(readlink -f ${DESTDIR}) -C ${.CURDIR}/contrib install; fi @(cd ${.CURDIR}/src 2> /dev/null && find * -type f) | while read FILE; do \ tar -C ${.CURDIR}/src -cpf - "$${FILE}" | \ tar -C ${DESTDIR}${LOCALBASE} -xpf -; \ @@ -240,7 +262,10 @@ install: check @cat ${TEMPLATESDIR}/version | sed ${SED_REPLACE} > "${DESTDIR}${LOCALBASE}/opnsense/version/${PLUGIN_NAME}" plist: check - @(cd ${.CURDIR}/src 2> /dev/null && find * -type f) | while read FILE; do \ + @(if [ -d ${.CURDIR}/contrib ]; then \ + ${MAKE} -C ${.CURDIR}/contrib plist; \ + fi; \ + (cd ${.CURDIR}/src 2> /dev/null && find * -type f) | while read FILE; do \ if [ -f "$${FILE}.in" ]; then continue; fi; \ FILE="$${FILE%%.in}"; PREFIX=""; \ if [ "$${FILE%%.sample}" != "$${FILE}" ]; then \ @@ -253,8 +278,9 @@ plist: check FILE="$${FILE}.gz"; \ fi; \ echo "$${PREFIX}${LOCALBASE}/$${FILE}"; \ - done - @echo "${LOCALBASE}/opnsense/version/${PLUGIN_NAME}" + done; \ + echo "${LOCALBASE}/opnsense/version/${PLUGIN_NAME}" \ + ) | sort description: check @if [ -f ${.CURDIR}/${PLUGIN_DESC} ]; then \ @@ -284,13 +310,6 @@ remove: check fi; \ done -WRKDIR?=${.CURDIR}/work -WRKSRC?=${WRKDIR}/src -PKGDIR?=${WRKDIR}/pkg - -ensure-workdirs: - @mkdir -p ${WRKSRC} ${PKGDIR} - package: check @rm -rf ${WRKSRC} @mkdir -p ${WRKSRC} ${PKGDIR} @@ -334,142 +353,20 @@ clean: check fi @rm -rf ${.CURDIR}/work -lint-desc: check - @if [ ! -f ${.CURDIR}/${PLUGIN_DESC} ]; then \ - echo ">>> Missing ${PLUGIN_DESC}"; exit 1; \ - fi - -lint-shell: - @for FILE in $$(find ${.CURDIR}/src -name "*.sh" -type f); do \ - if [ "$$(head $${FILE} | grep -c '^#!\/')" == "0" ]; then \ - echo "Missing shebang in $${FILE}"; exit 1; \ - fi; \ - sh -n "$${FILE}" || exit 1; \ - done - -lint-xml: - @find ${.CURDIR}/src \ - -name "*.xml" -type f -print0 | xargs -0 -n1 xmllint --noout - -lint-model: - @if [ -d ${.CURDIR}/src/opnsense/mvc/app/models ]; then for MODEL in $$(find ${.CURDIR}/src/opnsense/mvc/app/models -depth 3 \ - -name "*.xml"); do \ - (xmllint $${MODEL} --xpath '//*[@type and not(@type="ArrayField") and (not(Required) or Required="N") and Default]' 2> /dev/null | grep '^<' || true) | while read LINE; do \ - echo "$${MODEL}: $${LINE} has a spurious default value set"; \ - done; \ - (xmllint $${MODEL} --xpath '//*[@type and not(@type="ArrayField") and Default=""]' 2> /dev/null | grep '^<' || true) | while read LINE; do \ - echo "$${MODEL}: $${LINE} has an empty default value set"; \ - done; \ - (xmllint $${MODEL} --xpath '//*[@type and not(@type="ArrayField") and BlankDesc="None"]' 2> /dev/null | grep '^<' || true) | while read LINE; do \ - echo "$${MODEL}: $${LINE} blank description is the default"; \ - done; \ - (xmllint $${MODEL} --xpath '//*[@type and not(@type="ArrayField") and BlankDesc and Required="Y"]' 2> /dev/null | grep '^<' || true) | while read LINE; do \ - echo "$${MODEL}: $${LINE} blank description not applicable on required field"; \ - done; \ - (xmllint $${MODEL} --xpath '//*[@type and not(@type="ArrayField") and BlankDesc and Multiple="Y"]' 2> /dev/null | grep '^<' || true) | while read LINE; do \ - echo "$${MODEL}: $${LINE} blank description not applicable on multiple field"; \ - done; \ - (xmllint $${MODEL} --xpath '//*[@type and not(@type="ArrayField") and Multiple="N"]' 2> /dev/null | grep '^<' || true) | while read LINE; do \ - echo "$${MODEL}: $${LINE} Multiple=N is the default"; \ - done; \ - (xmllint $${MODEL} --xpath '//*[@type and not(@type="ArrayField") and OptionValues[default[not(@value)] or multiple[not(@value)] or required[not(@value)]]]' 2> /dev/null | grep '^<' || true) | while read LINE; do \ - echo "$${MODEL}: $${LINE} option element default/multiple/required without value attribute"; \ - done; \ - done; fi - -ACLBIN?= ${.CURDIR}/../../../core/Scripts/dashboard-acl.sh - -lint-acl: check -.if exists(${ACLBIN}) - @${ACLBIN} ${.CURDIR}/../../../core -.else - @echo "Did not find ACLBIN, please provide a core repository"; exit 1 -.endif - -lint-exec: check -.for DIR in ${.CURDIR}/src/opnsense/scripts ${.CURDIR}/src/etc/rc.d ${.CURDIR}/src/etc/rc.syshook.d -.if exists(${DIR}) - @find ${DIR} -type f ! -name "*.xml" -print0 | \ - xargs -0 -t -n1 test -x || \ - (echo "Missing executable permission in ${DIR}"; exit 1) -.endif -.endfor - -LINTBIN?= ${.CURDIR}/../../../core/contrib/parallel-lint/parallel-lint - -lint-php: check -.if exists(${LINTBIN}) - @if [ -d ${.CURDIR}/src ]; then ${LINTBIN} src; fi -.else - @echo "Did not find LINTBIN, please provide a core repository"; exit 1 -.endif - -lint: lint-desc lint-shell lint-xml lint-model lint-acl lint-exec lint-php - -plist-fix: - -sweep: check - find ${.CURDIR}/src -type f -name "*.map" -print0 | \ - xargs -0 -n1 rm - find ${.CURDIR}/src ! -name "*.min.*" ! -name "*.svg" \ - ! -name "*.ser" -type f -print0 | \ - xargs -0 -n1 ${SCRIPTSDIR}/cleanfile - find ${.CURDIR} -type f -depth 1 -print0 | \ - xargs -0 -n1 ${SCRIPTSDIR}/cleanfile - -glint: sweep style-fix plist-fix lint +glint: sweep lint revision: @MAKE=${MAKE} ${SCRIPTSDIR}/revbump.sh ${.CURDIR} -STYLEDIRS?= src/etc/inc src/opnsense - -style: check - @: > ${.CURDIR}/.style.out -.for STYLEDIR in ${STYLEDIRS} - @if [ -d ${.CURDIR}/${STYLEDIR} ]; then \ - (phpcs --standard=${PLUGINSDIR}/ruleset.xml \ - ${.CURDIR}/${STYLEDIR} || true) > \ - ${.CURDIR}/.style.out; \ - fi -.endfor - @echo -n "Total number of style warnings: " - @grep '| WARNING' ${.CURDIR}/.style.out | wc -l - @echo -n "Total number of style errors: " - @grep '| ERROR' ${.CURDIR}/.style.out | wc -l - @cat ${.CURDIR}/.style.out - @rm ${.CURDIR}/.style.out - -style-fix: check -.for STYLEDIR in ${STYLEDIRS} - @if [ -d ${.CURDIR}/${STYLEDIR} ]; then \ - phpcbf --standard=${PLUGINSDIR}/ruleset.xml \ - ${.CURDIR}/${STYLEDIR} || true; \ - fi -.endfor - -style-python: check - @if [ -d ${.CURDIR}/src ]; then \ - pycodestyle --ignore=E501 ${.CURDIR}/src || true; \ - fi - -style-model: - @for MODEL in $$(find ${.CURDIR}/src/opnsense/mvc/app/models -depth 3 \ - -name "*.xml"); do \ - perl -i -pe 's/(.*?)<\/default>/$$1<\/Default>/g' $${MODEL}; \ - perl -i -pe 's/(.*?)<\/multiple>/$$1<\/Multiple>/g' $${MODEL}; \ - perl -i -pe 's/(.*?)<\/required>/$$1<\/Required>/g' $${MODEL}; \ - done - test: check - @if [ -d ${.CURDIR}/src/opnsense/mvc/tests ]; then \ - cd ${LOCALBASE}/opnsense/mvc/tests && \ - phpunit --configuration PHPunit.xml \ - ${.CURDIR}/src/opnsense/mvc/tests; \ - fi +.if exists(${TESTDIR}) + @cd ${TESTDIR} && phpunit || true; \ + rm -rf ${TESTDIR}/.phpunit.result.cache +.endif -commit: ensure-workdirs +commit: + @mkdir -p ${MFCDIR} @/bin/echo -n "${.CURDIR:C/\// /g:[-2]}/${.CURDIR:C/\// /g:[-1]}: " > \ - ${WRKDIR}/.commitmsg && git commit -eF ${WRKDIR}/.commitmsg . + ${MFCDIR}/.commitmsg && git commit -eF ${MFCDIR}/.commitmsg . -.PHONY: check plist-fix +.PHONY: check diff --git a/Mk/style.mk b/Mk/style.mk new file mode 100644 index 000000000..e9dcc839f --- /dev/null +++ b/Mk/style.mk @@ -0,0 +1,28 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +CORE_PYTHON_DOT=${PLUGIN_PYTHON:C/./&./1} + +.-include "${PLUGINSDIR}/../core/Mk/style.mk" diff --git a/Mk/sweep.mk b/Mk/sweep.mk new file mode 100644 index 000000000..ced1d0ea3 --- /dev/null +++ b/Mk/sweep.mk @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +.-include "${PLUGINSDIR}/../core/Mk/sweep.mk" diff --git a/README.md b/README.md index fcc3df8e3..65f1a8a1a 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ emulators/qemu-guest-agent -- QEMU Guest Agent for OPNsense ftp/tftp -- TFTP server mail/postfix -- SMTP mail relay mail/rspamd -- Protect your network from spam -misc/theme-advanced -- OPNsense theme based on AdvancedTomato GUI +misc/theme-advanced -- Theme based on AdvancedTomato GUI misc/theme-cicada -- The cicada theme - dark grey onyx +misc/theme-flexcolor -- Theme with 3 different color schemes: black as default, light and dark-light misc/theme-rebellion -- A suitably dark theme misc/theme-tukan -- The tukan theme - blue/white misc/theme-vicuna -- The vicuna theme - blue sapphire @@ -55,6 +56,7 @@ net/google-cloud-sdk -- Google Cloud SDK net/haproxy -- Reliable, high performance TCP/HTTP load balancer net/igmp-proxy -- IGMP-Proxy Service net/mdns-repeater -- Proxy multicast DNS between networks +net/ndp-proxy-go -- IPv6 Neighbor Discovery Protocol Proxy net/ndproxy -- Neighbor Discovery Proxy net/ntopng -- Traffic Analysis and Flow Collection net/radsecproxy -- RADIUS proxy provides both RADIUS UDP and TCP/TLS (RadSec) transport @@ -64,8 +66,9 @@ net/shadowsocks -- Secure socks5 proxy net/siproxd -- Siproxd is a proxy daemon for the SIP protocol net/sslh -- sslh configuration front-end net/tayga -- Tayga NAT64 +net/turnserver -- The coturn STUN/TURN Server net/udpbroadcastrelay -- Control udpbroadcastrelay processes -net/upnp -- Universal Plug and Play (UPnP IGD & PCP/NAT-PMP) Service +net/upnp -- UPnP IGD & PCP/NAT-PMP Service net/vnstat -- Network traffic monitor net/wol -- Wake on LAN Service net/zerotier -- Virtual Networks That Just Work @@ -83,10 +86,14 @@ security/crowdsec -- Lightweight and collaborative security engine security/etpro-telemetry -- ET Pro Telemetry Edition security/intrusion-detection-content-et-open -- IDS Proofpoint full ET open ruleset complementary subset for ET Pro Telemetry edition security/intrusion-detection-content-et-pro -- IDS Proofpoint ET Pro ruleset (needs a valid subscription) +security/intrusion-detection-content-pt-open -- IDS Positive Technologies ESC ruleset security/intrusion-detection-content-snort-vrt -- IDS Snort VRT ruleset (needs registration or subscription) security/maltrail -- Malicious traffic detection system +security/netbird -- Peer-to-peer VPN that seamlessly connects your devices security/openconnect -- OpenConnect Client -security/softether -- Cross-platform Multi-protocol VPN Program (development only) +security/openvpn-legacy -- OpenVPN legacy support +security/q-feeds-connector -- Connector for Q-Feeds threat intel +security/strongswan-legacy -- IPsec legacy support security/stunnel -- Stunnel TLS proxy security/tailscale -- VPN mesh securely connecting clients using WireGuard security/tinc -- Tinc VPN @@ -94,9 +101,11 @@ security/tor -- The Onion Router security/wazuh-agent -- Agent for the open source security platform Wazuh sysutils/apcupsd -- APCUPSD - APC UPS daemon sysutils/apuled -- PC Engine APU LED control (development only) +sysutils/beats -- Send logs, network, metrics and heartbeat to Elasticsearch sysutils/cpu-microcode -- CPU microcode updates sysutils/dec-hw -- Deciso hardware specific information sysutils/dmidecode -- Display hardware information on the dashboard +sysutils/gdrive-backup -- Backup configurations using Google Drive sysutils/git-backup -- Track config changes using git sysutils/hw-probe -- Collect hardware diagnostics sysutils/lcdproc-sdeclcd -- LCDProc for SDEC LCD devices @@ -106,11 +115,12 @@ sysutils/nextcloud-backup -- Track config changes using NextCloud sysutils/node_exporter -- Prometheus exporter for machine metrics sysutils/nut -- Network UPS Tools sysutils/puppet-agent -- Manage Puppet Agent +sysutils/sftp-backup -- Backup configurations using SFTP sysutils/smart -- SMART tools sysutils/virtualbox -- VirtualBox guest additions sysutils/vmware -- VMware tools sysutils/xen -- Xen guest utilities -vendor/sunnyvalley -- Vendor Repository for Zenarmor (a.k.a Sensei, Next Generation Firewall Extensions) +vendor/sunnyvalley -- Vendor Repository for Zenarmor (Enterprise Security Modules - NGFW, SSE, SASE, f.k.a Sensei) www/OPNProxy -- OPNsense proxy additions www/c-icap -- c-icap connects the web proxy with a virus scanner www/cache -- Webserver cache @@ -142,9 +152,8 @@ The make targets for the root directory: * clean: remove all changes and unknown files * lint: run syntax checks * list: print a list of all plugin directories with comments -* style-fix: apply style fixes * style: run style checks -* sweep: apply whitespace fixes +* sweep: apply style fixes The make targets for any plugin directory: @@ -155,6 +164,5 @@ The make targets for any plugin directory: * package: creates a package * upgrade: upgrades existing package * remove: remove known files from target directory -* style-fix: apply style fixes * style: run style checks -* sweep: apply whitespace fixes +* sweep: apply style fixes diff --git a/Scripts/cleanfile b/Scripts/cleanfile deleted file mode 100755 index 8abf76025..000000000 --- a/Scripts/cleanfile +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env perl -# -# Clean a text file -- or directory of text files -- of stealth whitespace. -# WARNING: this can be a highly destructive operation. Use with caution. -# - -use bytes; -use File::Basename; - -# Default options -$max_width = 0; - -# Clean up space-tab sequences, either by removing spaces or -# replacing them with tabs. -sub clean_space_tabs($) -{ - no bytes; # Tab alignment depends on characters - - my($li) = @_; - my($lo) = ''; - my $pos = 0; - my $nsp = 0; - my($i, $c); - - for ($i = 0; $i < length($li); $i++) { - $c = substr($li, $i, 1); - if ($c eq "\t") { - my $npos = ($pos+$nsp+8) & ~7; - my $ntab = ($npos >> 3) - ($pos >> 3); - $lo .= "\t" x $ntab; - $pos = $npos; - $nsp = 0; - } elsif ($c eq "\n" || $c eq "\r") { - $lo .= " " x $nsp; - $pos += $nsp; - $nsp = 0; - $lo .= $c; - $pos = 0; - } elsif ($c eq " ") { - $nsp++; - } else { - $lo .= " " x $nsp; - $pos += $nsp; - $nsp = 0; - $lo .= $c; - $pos++; - } - } - $lo .= " " x $nsp; - return $lo; -} - -# Compute the visual width of a string -sub strwidth($) { - no bytes; # Tab alignment depends on characters - - my($li) = @_; - my($c, $i); - my $pos = 0; - my $mlen = 0; - - for ($i = 0; $i < length($li); $i++) { - $c = substr($li,$i,1); - if ($c eq "\t") { - $pos = ($pos+8) & ~7; - } elsif ($c eq "\n") { - $mlen = $pos if ($pos > $mlen); - $pos = 0; - } else { - $pos++; - } - } - - $mlen = $pos if ($pos > $mlen); - return $mlen; -} - -$name = basename($0); - -@files = (); - -while (defined($a = shift(@ARGV))) { - if ($a =~ /^-/) { - if ($a eq '-width' || $a eq '-w') { - $max_width = shift(@ARGV)+0; - } else { - print STDERR "Usage: $name [-width #] files...\n"; - exit 1; - } - } else { - push(@files, $a); - } -} - -foreach $f ( @files ) { - print STDERR "$name: $f\n"; - - if (! -f $f) { - print STDERR "$f: not a file\n"; - next; - } - - if (!open(FILE, '+<', $f)) { - print STDERR "$name: Cannot open file: $f: $!\n"; - next; - } - - binmode FILE; - - # First, verify that it is not a binary file; consider any file - # with a zero byte to be a binary file. Is there any better, or - # additional, heuristic that should be applied? - $is_binary = 0; - - while (read(FILE, $data, 65536) > 0) { - if ($data =~ /\0/) { - $is_binary = 1; - last; - } - } - - if ($is_binary) { - print STDERR "$name: $f: binary file\n"; - next; - } - - seek(FILE, 0, 0); - - $in_bytes = 0; - $out_bytes = 0; - $blank_bytes = 0; - - @blanks = (); - @lines = (); - $last = "\n"; - $lineno = 0; - - while ( defined($line = ) ) { - $lineno++; - $in_bytes += length($line); - $line =~ s/[ \t\r]*$//; # Remove trailing spaces - $line = clean_space_tabs($line); - $last = $line; - - if ( $line eq "\n" ) { - push(@blanks, $line); - $blank_bytes += length($line); - } else { - push(@lines, @blanks); - $out_bytes += $blank_bytes; - push(@lines, $line); - $out_bytes += length($line); - @blanks = (); - $blank_bytes = 0; - } - - $l_width = strwidth($line); - if ($max_width && $l_width > $max_width) { - print STDERR - "$f:$lineno: line exceeds $max_width characters ($l_width)\n"; - } - } - - if ( chop($last) ne "\n" ) { - # fix missing newline at EOF - push(@lines, "\n"); - # increment input bytes to signal character append - $in_bytes += 1; - } - - # Any blanks at the end of the file are discarded - - if ($in_bytes != $out_bytes) { - # Only write to the file if changed - seek(FILE, 0, 0); - print FILE @lines; - - if ( !defined($where = tell(FILE)) || - !truncate(FILE, $where) ) { - die "$name: Failed to truncate modified file: $f: $!\n"; - } - } - - close(FILE); -} diff --git a/Scripts/license b/Scripts/license index 6580d40b2..38d61f260 100755 --- a/Scripts/license +++ b/Scripts/license @@ -67,6 +67,7 @@ sub process_file my $filename = $File::Find::name; return if not -f "$cwd/$filename"; + return if $filename =~ /\/Private\//; my @lines = read_file( "$cwd/$filename" ); my $possibly_bsd; diff --git a/benchmarks/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.xml b/benchmarks/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.xml index 939f4e21f..1ca2f112e 100644 --- a/benchmarks/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.xml +++ b/benchmarks/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.xml @@ -3,9 +3,9 @@ Fake model for the API - will be never stored to config (only used for defaults, validation etc.). - lan + lan Y - N + N diff --git a/databases/redis/src/opnsense/mvc/app/models/OPNsense/Redis/Redis.xml b/databases/redis/src/opnsense/mvc/app/models/OPNsense/Redis/Redis.xml index 0750ed1d8..2a2f05d31 100644 --- a/databases/redis/src/opnsense/mvc/app/models/OPNsense/Redis/Redis.xml +++ b/databases/redis/src/opnsense/mvc/app/models/OPNsense/Redis/Redis.xml @@ -4,27 +4,27 @@ - 0 + 0 Y N - Y + Y - 1 + 1 Y 1 65536 N - 6379 + 6379 This must be a valid port number. Y - warning + warning Debug Verbose @@ -33,12 +33,12 @@ - 0 + 0 Y Y - LOCAL0 + LOCAL0 USER LOCAL0 @@ -54,7 +54,7 @@ 0 Y - 16 + 16 @@ -68,7 +68,7 @@ 0 N - 10000 + 10000 0 @@ -76,7 +76,7 @@ Y - noeviction + noeviction noeviction volatile-ttl @@ -89,19 +89,19 @@ 0 N - 5 + 5 0 N - 10000 + 10000 0 N - 128 + 128 diff --git a/devel/grid_example/Makefile b/devel/grid_example/Makefile index 3b7bda5db..f6c9214c6 100644 --- a/devel/grid_example/Makefile +++ b/devel/grid_example/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= grid_example -PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.1 PLUGIN_COMMENT= A sample framework application PLUGIN_MAINTAINER= ad@opnsense.org diff --git a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/ServiceController.php b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/ServiceController.php new file mode 100644 index 000000000..00ae072e0 --- /dev/null +++ b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/ServiceController.php @@ -0,0 +1,49 @@ + "ok"]; + } +} diff --git a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/SettingsController.php b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/SettingsController.php index 3cbe4725d..a7f63c2dd 100644 --- a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/SettingsController.php +++ b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/SettingsController.php @@ -1,7 +1,7 @@ searchBase("addresses.address", array('enabled', 'email'), "email"); + return $this->searchBase("addresses.address", null, "email"); } public function setItemAction($uuid) diff --git a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/IndexController.php b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/IndexController.php index e3e86e647..79d674d0b 100644 --- a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/IndexController.php +++ b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/IndexController.php @@ -1,7 +1,7 @@ view->pick('OPNsense/GridExample/index'); $this->view->formDialogAddress = $this->getForm("dialogAddress"); + // convert dialog for grid table + $this->view->formGridAddress = $this->getFormGrid("dialogAddress"); } } diff --git a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/forms/dialogAddress.xml b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/forms/dialogAddress.xml index 9a65a91a7..74f842bd7 100644 --- a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/forms/dialogAddress.xml +++ b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/forms/dialogAddress.xml @@ -1,13 +1,30 @@
address.enabled - + checkbox Enable this address + + + 6em + boolean + rowtoggle + address.email text + Enter the email address + + + address.description + + text + Enter an optional description + + + false +
diff --git a/devel/grid_example/src/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.xml b/devel/grid_example/src/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.xml index 7208836b3..fc1146dd2 100644 --- a/devel/grid_example/src/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.xml +++ b/devel/grid_example/src/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.xml @@ -7,12 +7,13 @@
- 1 + 1 Y Y +
diff --git a/devel/grid_example/src/opnsense/mvc/app/views/OPNsense/GridExample/index.volt b/devel/grid_example/src/opnsense/mvc/app/views/OPNsense/GridExample/index.volt index 0061bcb4f..4f0ed8fd5 100644 --- a/devel/grid_example/src/opnsense/mvc/app/views/OPNsense/GridExample/index.volt +++ b/devel/grid_example/src/opnsense/mvc/app/views/OPNsense/GridExample/index.volt @@ -1,5 +1,5 @@ {# - # Copyright (c) 2019 Deciso B.V. + # Copyright (c) 2019-2025 Deciso B.V. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -25,43 +25,29 @@ #} - - - - - - - - - - - - - - - - - - -
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Email') }}{{ lang._('Commands') }}
- - -
- - -{{ partial("layout_partials/base_dialog",['fields':formDialogAddress,'id':'DialogAddress','label':lang._('Edit address')])}} +
+ + {{ partial('layout_partials/base_bootgrid_table', formGridAddress) }} +
+ +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/gridexample/service/reconfigure'}) }} + +{{ partial("layout_partials/base_dialog",['fields':formDialogAddress,'id':formGridAddress['edit_dialog_id'],'label':lang._('Edit address')])}} diff --git a/devel/helloworld/Makefile b/devel/helloworld/Makefile index a7e4092dc..86a8690ad 100644 --- a/devel/helloworld/Makefile +++ b/devel/helloworld/Makefile @@ -1,5 +1,6 @@ PLUGIN_NAME= helloworld PLUGIN_VERSION= 1.4 +PLUGIN_REVISION= 1 PLUGIN_COMMENT= A sample framework application PLUGIN_MAINTAINER= ad@opnsense.org diff --git a/devel/helloworld/src/opnsense/mvc/app/models/OPNsense/HelloWorld/HelloWorld.xml b/devel/helloworld/src/opnsense/mvc/app/models/OPNsense/HelloWorld/HelloWorld.xml index 097da5243..825a579b4 100644 --- a/devel/helloworld/src/opnsense/mvc/app/models/OPNsense/HelloWorld/HelloWorld.xml +++ b/devel/helloworld/src/opnsense/mvc/app/models/OPNsense/HelloWorld/HelloWorld.xml @@ -8,14 +8,14 @@ - 1 + 1 Y Y - sample@example.com + sample@example.com Y diff --git a/devel/helloworld/src/opnsense/scripts/OPNsense/HelloWorld/testConnection.py b/devel/helloworld/src/opnsense/scripts/helloworld/testConnection.py similarity index 100% rename from devel/helloworld/src/opnsense/scripts/OPNsense/HelloWorld/testConnection.py rename to devel/helloworld/src/opnsense/scripts/helloworld/testConnection.py diff --git a/devel/helloworld/src/opnsense/service/conf/actions.d/actions_helloworld.conf b/devel/helloworld/src/opnsense/service/conf/actions.d/actions_helloworld.conf index c5c521793..1bb2afb59 100644 --- a/devel/helloworld/src/opnsense/service/conf/actions.d/actions_helloworld.conf +++ b/devel/helloworld/src/opnsense/service/conf/actions.d/actions_helloworld.conf @@ -1,5 +1,5 @@ [test] -command:/usr/local/opnsense/scripts/OPNsense/HelloWorld/testConnection.py +command:/usr/local/opnsense/scripts/helloworld/testConnection.py parameters: type:script_output message:hello world module test diff --git a/dns/bind/Makefile b/dns/bind/Makefile index b2d897b0f..4d98f7a4b 100644 --- a/dns/bind/Makefile +++ b/dns/bind/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= bind -PLUGIN_VERSION= 1.33 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.34 +PLUGIN_REVISION= 2 PLUGIN_COMMENT= BIND domain name service PLUGIN_DEPENDS= bind920 PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/dns/bind/pkg-descr b/dns/bind/pkg-descr index 3457965b8..d3cbd8afa 100644 --- a/dns/bind/pkg-descr +++ b/dns/bind/pkg-descr @@ -9,6 +9,12 @@ WWW: https://www.isc.org Plugin Changelog ================ +1.34 + +* Add custom configuration include directory /usr/local/etc/namedb/named.conf.d (contributed by Nicholas Card) +* Add forward zones +* Fix primary zones grid and command column (contributed by benyamin-codez) + 1.33 * Add option to allow the rndc-key for zone transfers (contributed by Naomi Rennie-Waldock) diff --git a/dns/bind/src/etc/namedb/named.conf.d/00-README.conf b/dns/bind/src/etc/namedb/named.conf.d/00-README.conf new file mode 100644 index 000000000..fac2f0d6b --- /dev/null +++ b/dns/bind/src/etc/namedb/named.conf.d/00-README.conf @@ -0,0 +1,7 @@ +# Custom BIND Configuration Directory +# +# Place your custom BIND directives in .conf files in this directory +# Files are included in alphabetical order +# +# Examples: +# server 192.168.1.100 { edns no; }; diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/DomainController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/DomainController.php index 841588c02..87c71db3f 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/DomainController.php +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/DomainController.php @@ -73,6 +73,18 @@ class DomainController extends ApiMutableModelControllerBase ); } + public function searchForwardDomainAction() + { + return $this->searchBase( + 'domains.domain', + [ 'enabled', 'type', 'domainname', 'forwardserver' ], + 'domainname', + function ($record) { + return $record->type->getNodeData()['forward']['selected'] === 1; + } + ); + } + public function getDomainAction($uuid = null) { return $this->getBase('domain', 'domains.domain', $uuid); @@ -88,6 +100,11 @@ class DomainController extends ApiMutableModelControllerBase return $this->addBase('domain', 'domains.domain', ['type' => 'secondary']); } + public function addForwardDomainAction($uuid = null) + { + return $this->addBase('domain', 'domains.domain', ['type' => 'forward']); + } + public function delDomainAction($uuid) { return $this->delBase('domains.domain', $uuid); diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/GeneralController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/GeneralController.php index 663acb058..cfa86649d 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/GeneralController.php +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/GeneralController.php @@ -37,6 +37,7 @@ class GeneralController extends \OPNsense\Base\IndexController $this->view->formDialogEditBindAcl = $this->getForm("dialogEditBindAcl"); $this->view->formDialogEditBindPrimaryDomain = $this->getForm("dialogEditBindPrimaryDomain"); $this->view->formDialogEditBindSecondaryDomain = $this->getForm("dialogEditBindSecondaryDomain"); + $this->view->formDialogEditBindForwardDomain = $this->getForm("dialogEditBindForwardDomain"); $this->view->formDialogEditBindRecord = $this->getForm("dialogEditBindRecord"); $this->view->pick('OPNsense/Bind/general'); } diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindForwardDomain.xml b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindForwardDomain.xml new file mode 100644 index 000000000..4ca30f87c --- /dev/null +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindForwardDomain.xml @@ -0,0 +1,22 @@ +
+ + domain.enabled + + checkbox + This will enable or disable this zone. + + + domain.domainname + + text + Set the name for this zone. Both forward and reverse zones may be specified, i.e. example.com or 0.168.192.in-addr.arpa. + + + domain.forwardserver + + + select_multiple + true + Set the IP address of server to forward requests to. + +
diff --git a/sysutils/dmidecode/src/www/widgets/widgets/dmidecode.widget.php b/dns/bind/src/opnsense/mvc/app/library/OPNsense/System/Status/BindOverrideStatus.php similarity index 60% rename from sysutils/dmidecode/src/www/widgets/widgets/dmidecode.widget.php rename to dns/bind/src/opnsense/mvc/app/library/OPNsense/System/Status/BindOverrideStatus.php index c1fc7723e..eb15970e6 100644 --- a/sysutils/dmidecode/src/www/widgets/widgets/dmidecode.widget.php +++ b/dns/bind/src/opnsense/mvc/app/library/OPNsense/System/Status/BindOverrideStatus.php @@ -1,7 +1,8 @@ - - - - $val) { ?> - - - - - - - $val) { ?> - - - - - - -
+class BindOverrideStatus extends AbstractStatus +{ + public function __construct() + { + $this->internalPriority = 2; + $this->internalPersistent = true; + $this->internalIsBanner = true; + $this->internalTitle = gettext('BIND config override'); + $this->internalScope = [ + '/ui/bind/general/index' + ]; + } + + public function collectStatus() + { + if (count(glob('/usr/local/etc/namedb/named.conf.d/*')) > 1) { + $this->internalMessage = gettext( + 'The configuration contains manual overwrites, these may interfere with the settings configured here.' + ); + $this->internalStatus = SystemStatusCode::NOTICE; + } + } +} diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml index 870ee9ec0..a628f28a5 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml @@ -11,7 +11,7 @@ Y - /^(?!any$|localhost$|localnets$|none$)[0-9a-zA-Z_\-]{1,32}$/u + /^(?!any$|localhost$|localnets$|none$)[0-9a-zA-Z_\-]{1,32}$/u Should be a string between 1 and 32 characters. Allowed characters are 0-9, a-z, A-Z, _ and -. Built-in ACL names must not be used: any, localhost, localnets, none. @@ -21,9 +21,8 @@ - , Y - Y + Y diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Domain.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Domain.xml index 4f885f4ba..6743b66ae 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Domain.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Domain.xml @@ -1,7 +1,7 @@ //OPNsense/bind/domain BIND domain configuration - 1.1.1 + 1.1.2 @@ -15,12 +15,15 @@ primary secondary + forward - , - Y + Y + + Y + HMAC-SHA512 @@ -34,8 +37,7 @@ - , - Y + Y Y diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml index 72e771fbc..238c9dc24 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml @@ -17,15 +17,13 @@ 0.0.0.0 - , Y - Y + Y :: - , Y - Y + Y ipv4 @@ -48,8 +46,7 @@ Y - , - Y + Y 0 @@ -60,8 +57,7 @@ Y - , - Y + Y 5 @@ -152,9 +148,8 @@ 0.0.0.0,:: - , Y - Y + Y Y diff --git a/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/general.volt b/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/general.volt index 0649c0eb2..0d9b55eda 100644 --- a/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/general.volt +++ b/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/general.volt @@ -33,6 +33,7 @@
  • {{ lang._('ACLs') }}
  • {{ lang._('Primary Zones') }}
  • {{ lang._('Secondary Zones') }}
  • +
  • {{ lang._('Forward Zones') }}
  • @@ -99,7 +100,7 @@ {{ lang._('Expire') }} {{ lang._('Negative TTL') }} {{ lang._('ID') }} - {{ lang._('Commands') }} + {{ lang._('Commands') }} @@ -189,11 +190,48 @@

    +
    +
    +

    {{ lang._('Zones') }}

    +
    +
    + + + + + + + + + + + + + + + + + + +
    {{ lang._('Enabled') }}{{ lang._('Zone') }}{{ lang._('Forwarder IPs') }}{{ lang._('ID') }}{{ lang._('Commands') }}
    + +
    +
    +
    +
    + + +

    +
    +
    {{ partial("layout_partials/base_dialog",['fields':formDialogEditBindAcl,'id':'dialogEditBindAcl','label':lang._('Edit ACL')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogEditBindPrimaryDomain,'id':'dialogEditBindPrimaryDomain','label':lang._('Edit Primary Zone')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogEditBindSecondaryDomain,'id':'dialogEditBindSecondaryDomain','label':lang._('Edit Secondary Zone')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditBindForwardDomain,'id':'dialogEditBindForwardDomain','label':lang._('Edit Forward Zone')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogEditBindRecord,'id':'dialogEditBindRecord','label':lang._('Edit Record')])}} + account.resourceId diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.php b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.php index 31682aae0..a3cbb5a86 100644 --- a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.php +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.php @@ -52,6 +52,16 @@ class DynDNS extends BaseModel } } foreach ($validate_servers as $key => $node) { + if ((string)$node->service == 'powerdns') { + if (empty($srv) || filter_var($srv, FILTER_VALIDATE_URL) === false) { + $messages->appendMessage( + new Message( + gettext("A valid URI is required."), + $key . ".server" + ) + ); + } + } if ((string)$node->service != 'custom') { continue; } diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml index 600e17517..d1a19de7c 100644 --- a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml @@ -105,16 +105,16 @@ N - /^([a-zA-Z0-9\-.@_:+\%])*$/u + /^([a-zA-Z0-9\-.@_:+\%])*$/u The username contains invalid characters. N - /^[^\n]*$/ + /^[^\n]*$/ N - /^[^\n]*$/ + /^[^\n]*$/ resourceId contains invalid characters. @@ -139,15 +139,17 @@ web_dyndns An IP service type is required. + akamai + akamai-ipv4 + akamai-ipv6 cloudflare cloudflare-ipv4 cloudflare-ipv6 - dyndns + dynu-ipv4 + dynu-ipv6 freedns he icanhazip - ip4only.me - ip6only.me ipify-ipv4 ipify-ipv6 loopia @@ -167,7 +169,7 @@ N - /^::(([0-9a-fA-F]{1,4}:){0,3}[0-9a-fA-F]{1,4})?$/u + /^::(([0-9a-fA-F]{1,4}:){0,3}[0-9a-fA-F]{1,4})?$/u Entry is not a valid partial ipv6 address definition (e.g. ::1000). @@ -199,9 +201,11 @@ N - /^(.){1,255}$/u + /^(.){1,255}$/u Description should be a string between 1 and 255 characters + +
    diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/AccountField.php b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/AccountField.php index 32ec0c854..069768432 100644 --- a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/AccountField.php +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/AccountField.php @@ -40,29 +40,22 @@ class AccountField extends ArrayField private function addStatsFields($node) { - // generate new unattached fields, which are only usable to read data from (not synched to config.xml) - $current_ip = new TextField(); - $current_ip->setInternalIsVirtual(); - $current_mtime = new TextField(); - $current_mtime->setInternalIsVirtual(); if (isset(self::$current_stats[$node->getAttribute('uuid')])) { $stats = self::$current_stats[$node->getAttribute('uuid')]; if (!empty($stats)) { - $current_ip->setValue($stats['ip']); - $current_mtime->setValue(date('c', (int)$stats['mtime'])); + $node->current_ip->setValue($stats['ip']); + $node->current_mtime->setValue(date('c', (int)$stats['mtime'])); } } elseif (!empty((string)$node->hostnames)) { foreach (explode(",", (string)$node->hostnames) as $hostname) { if (!empty(self::$current_stats[$hostname]) && !empty(self::$current_stats[$hostname]['ip'])) { $stats = self::$current_stats[$hostname]; - $current_ip->setValue($stats['ip']); - $current_mtime->setValue(date('c', $stats['mtime'])); + $node->current_ip->setValue($stats['ip']); + $node->current_mtime->setValue(date('c', $stats['mtime'])); break; } } } - $node->addChildNode('current_ip', $current_ip); - $node->addChildNode('current_mtime', $current_mtime); } protected function actionPostLoadingEvent() diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py index 7b600eb57..811733ace 100755 --- a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py @@ -122,7 +122,7 @@ class BaseAccount: dynipv6host = self.settings['dynipv6host'] if self.settings.get('dynipv6host' ,'').strip() != '' else None ) - if self._current_address == None: + if not self._current_address: syslog.syslog( syslog.LOG_WARNING, "Account %s no global IP address detected, check config if warning persists" % (self.description) diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/powerdns.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/powerdns.py new file mode 100755 index 000000000..ea931a0cf --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/powerdns.py @@ -0,0 +1,209 @@ +""" + Copyright (c) 2023 Ad Schellevis + Copyright (c) 2024 Olly Baker + Copyright (c) 2025 Oliver Traber + 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. +""" +import syslog +import requests +from . import BaseAccount + + +class PowerDNS(BaseAccount): + + def __init__(self, account: dict): + super().__init__(account) + # min TTL set to 300 + self.settings['ttl'] = max(int(self.settings["ttl"]) if self.settings.get("ttl", "").isdigit() else 0, 300) + + @staticmethod + def known_services(): + return {"powerdns": "PowerDNS API"} + + @staticmethod + def match(account): + return account.get("service") in ['powerdns'] + + + def _send_request(self, method, url, params=None, json=None): + headers = { + "User-Agent": "OPNsense-dyndns", + "X-API-Key": self.settings.get("password"), + } + + base_url = "%s/api/v1/servers/%s" % ( + self.settings.get('server'), + self.settings.get("server_id", "localhost") + ) + + url = base_url + url + return requests.request(method=method, url=url, headers=headers, params=params, json=json) + + + def _find_zone_id(self, hostname): + # Get the zone that a record belongs to + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s trying to get zone ID for hostname %s" + % (self.description, hostname), + ) + + zone = hostname + while (True): + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s checking if zone %s exists" + % (self.description, zone), + ) + + response = self._send_request(method="GET", url="/zones", params={"zone": zone}) + + if response.status_code == 200: + try: + payload = response.json() + # Check if a zone was found + if len(payload) == 0: + # Move one up in hierarchy + zone = '.'.join(zone.split('.')[1:]) + # Fail if root is reached + if zone == "": + syslog.syslog( + syslog.LOG_ERR, + "Account %s error getting zone ID for hostname %s - No matching zone found on server" + % (self.description, hostname), + ) + return None + else: + continue + else: + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s found zone %s for hostname %s" + % (self.description, zone, hostname), + ) + return payload[0].get("id") + except requests.exceptions.JSONDecodeError: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error getting zone ID for hostname %s - Failed to decode response as JSON. Response: %s" + % (self.description, hostname, response.text), + ) + return None + else: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error getting zone ID for hostname %s HTTP %s. Response: %s" + % ( + self.description, + hostname, + response.status_code, + response.text, + ), + ) + return None + + def _replace_rrset(self, hostname, zone_id, record_type, content): + # Replace or create rrset for record + payload = { + "rrsets": [ + { + "name": hostname, + "type": record_type, + "ttl": int(self.settings.get("ttl")), + "changetype": "REPLACE", + "records": [ + {"content": content} + ] + } + ] + } + + response = self._send_request(method="PATCH", url=("/zones/" + zone_id), json=payload) + if response.status_code == 204: + # Success + return True + else: + # Failure + syslog.syslog( + syslog.LOG_ERR, + "Account %s error updating hostname %s in zone %s - HTTP %s Response: %s" + % ( + self.description, + hostname, + zone_id, + response.status_code, + response.text, + ), + ) + return False + + def execute(self): + if super().execute(): + record_type = "AAAA" if str(self.current_address).find(":") > 1 else "A" + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s current IP is %s (%s)" + % (self.description, self.current_address, record_type), + ) + + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s updating hostnames %s" + % (self.description, self.settings.get("hostnames", "")), + ) + + # Update each hostname + for hostname in self.settings.get("hostnames", "").split(","): + + # Make hostname absolute + if not hostname.endswith("."): + hostname = hostname + "." + + # Get id of zone the hostname belongs to + zone_id = self._find_zone_id(hostname) + + # If zone can't be found, skip + if zone_id == None: + continue + + # Update record set + if self._replace_rrset(hostname, zone_id, record_type, content=self.current_address) and self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s successfully updated hostname %s (%s) to IP %s" + % ( + self.description, + hostname, + record_type, + self.current_address, + ), + ) + self.update_state(address=self.current_address) + return True + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py index d1dc1c6ab..44ad79299 100755 --- a/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py @@ -29,15 +29,17 @@ import ipaddress from urllib.parse import urlparse checkip_service_list = { + 'akamai': '%s://whatismyip.akamai.com', + 'akamai-ipv4': '%s://ipv4.whatismyip.akamai.com', + 'akamai-ipv6': '%s://ipv6.whatismyip.akamai.com', 'cloudflare': '%s://one.one.one.one/cdn-cgi/trace', 'cloudflare-ipv4': '%s://1.1.1.1/cdn-cgi/trace', 'cloudflare-ipv6': '%s://[2606:4700:4700::1111]/cdn-cgi/trace', - 'dyndns': '%s://checkip.dyndns.org/', + 'dynu-ipv4': '%s://ipcheck.dynu.com/', + 'dynu-ipv6': '%s://ipcheckv6.dynu.com/', 'freedns': '%s://freedns.afraid.org/dynamic/check.php', 'he': '%s://checkip.dns.he.net/', 'icanhazip': '%s://icanhazip.com/', - 'ip4only.me': '%s://ip4only.me/api/', - 'ip6only.me': '%s://ip6only.me/api/', 'ipify-ipv4': '%s://api.ipify.org/', 'ipify-ipv6': '%s://api6.ipify.org/', 'loopia': '%s://dns.loopia.se/checkip/checkip.php', diff --git a/dns/ddclient/src/opnsense/service/conf/actions.d/actions_ddclient.conf b/dns/ddclient/src/opnsense/service/conf/actions.d/actions_ddclient.conf index 8500bfa48..8349ebdfe 100644 --- a/dns/ddclient/src/opnsense/service/conf/actions.d/actions_ddclient.conf +++ b/dns/ddclient/src/opnsense/service/conf/actions.d/actions_ddclient.conf @@ -6,7 +6,8 @@ type:script message:starting ddclient [stop] -command:pkill -F /var/run/ddclient.pid 2> /dev/null; /usr/local/etc/rc.d/ddclient_opn onestop 2> /dev/null; exit 0 +command:pkill -F /var/run/ddclient.pid 2> /dev/null; /usr/local/etc/rc.d/ddclient_opn onestop 2> /dev/null +errors:no type:script message:stopping ddclient diff --git a/dns/dnscrypt-proxy/Makefile b/dns/dnscrypt-proxy/Makefile index 2f982bea5..d0b5eeb8b 100644 --- a/dns/dnscrypt-proxy/Makefile +++ b/dns/dnscrypt-proxy/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= dnscrypt-proxy -PLUGIN_VERSION= 1.15 -PLUGIN_REVISION= 2 +PLUGIN_VERSION= 1.16 PLUGIN_COMMENT= Flexible DNS proxy supporting DNSCrypt and DoH PLUGIN_DEPENDS= dnscrypt-proxy2 PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/dns/dnscrypt-proxy/pkg-descr b/dns/dnscrypt-proxy/pkg-descr index 90d55760d..f06697ce8 100644 --- a/dns/dnscrypt-proxy/pkg-descr +++ b/dns/dnscrypt-proxy/pkg-descr @@ -1,10 +1,13 @@ A flexible DNS proxy, with support for modern encrypted DNS protocols such as DNSCrypt v2 and DNS-over-HTTPS. - Plugin Changelog ================ +1.16 + +* Fix ODoH servers not working (contributed by Pascal Herget) + 1.15 * Update plugin for dnscrypt-proxy 2.1 (contributed by Adrian Fedoreanu and Michael Muenz) diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/forms/general.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/forms/general.xml index 7f5d0e853..02b73663a 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/forms/general.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/forms/general.xml @@ -49,6 +49,12 @@ checkbox Let DNSCrypt-Proxy use servers with DNS-over-HTTPS protocol enabled. + + general.odoh_servers + + checkbox + Let DNSCrypt-Proxy use servers with Oblivious-DNS-over-HTTPS protocol enabled. Note: If checked you must provide ODoH target and relay servers manually! + general.require_dnssec diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Cloak.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Cloak.xml index fd1c125db..a76121b98 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Cloak.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Cloak.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml index 95a89c128..188564ee6 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml @@ -4,11 +4,10 @@ 1.0.0 - 0 + 0 Y - N Y AdAway List diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Forward.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Forward.xml index 5632cc3a6..243cca945 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Forward.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Forward.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.xml index f7a64b907..b942d30b0 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.xml @@ -1,153 +1,149 @@ //OPNsense/dnscryptproxy/general dnscrypt-proxy configuration - 0.1.2 + 0.1.3 - 0 + 0 Y - 0.0.0.0:5353 + 0.0.0.0:5353 Y - 1 + 1 Y - 250 + 250 Y 1 10000 Choose a number between 1 and 10000. - 1 + 1 Y - 0 + 0 Y - 1 + 1 Y - 1 + 1 Y + + 0 + Y + - 0 + 0 Y - 1 + 1 Y - 0 + 0 Y - 0 + 0 Y - - N - + - 2500 + 2500 Y 100 10000 Choose a number between 100 and 10000. - 30 + 30 Y 1 600 Choose a number between 1 and 600. - 240 + 240 Y 1 3600 Choose a number between 1 and 3600. - 0 + 0 Y - 0 + 0 Y - 9.9.9.9:53 + 9.9.9.9:53 Y - 0 + 0 Y - 1 + 1 Y - 512 + 512 Y 1 20480 Choose a number between 1 and 20480. - 600 + 600 Y 1 3600 Choose a number between 1 and 3600. - 86400 + 86400 Y 1 86400 Choose a number between 1 and 86400. - 60 + 60 Y 1 3600 Choose a number between 1 and 3600. - 600 + 600 Y 1 86400 Choose a number between 1 and 86400. - - N - + - 1 + 1 Y - /^[A-Za-z0-9\._\-]{1,70}(,[A-Za-z0-9\._\-]{1,70})*$/ - - N + /^[A-Za-z0-9\._\-]{1,70}(,[A-Za-z0-9\._\-]{1,70})*$/ Please use valid server names. - - N - + diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Server.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Server.xml index 873cfaf6e..2af1f6abb 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Server.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Server.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Whitelist.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Whitelist.xml index f13e2463d..c3d65113e 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Whitelist.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Whitelist.xml @@ -6,15 +6,13 @@ - 1 + 1 Y Y - - N - + diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/views/OPNsense/Dnscryptproxy/general.volt b/dns/dnscrypt-proxy/src/opnsense/mvc/app/views/OPNsense/Dnscryptproxy/general.volt index 606b63196..c73357a0b 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/views/OPNsense/Dnscryptproxy/general.volt +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/views/OPNsense/Dnscryptproxy/general.volt @@ -186,42 +186,42 @@ $( document ).ready(function() { }); $("#grid-forwards").UIBootgrid( - { 'search':'/api/dnscryptproxy/forward/searchForward', - 'get':'/api/dnscryptproxy/forward/getForward/', - 'set':'/api/dnscryptproxy/forward/setForward/', - 'add':'/api/dnscryptproxy/forward/addForward/', - 'del':'/api/dnscryptproxy/forward/delForward/', - 'toggle':'/api/dnscryptproxy/forward/toggleForward/' + { 'search':'/api/dnscryptproxy/forward/search_forward', + 'get':'/api/dnscryptproxy/forward/get_forward/', + 'set':'/api/dnscryptproxy/forward/set_forward/', + 'add':'/api/dnscryptproxy/forward/add_forward/', + 'del':'/api/dnscryptproxy/forward/del_forward/', + 'toggle':'/api/dnscryptproxy/forward/toggle_forward/' } ); $("#grid-cloaks").UIBootgrid( - { 'search':'/api/dnscryptproxy/cloak/searchCloak', - 'get':'/api/dnscryptproxy/cloak/getCloak/', - 'set':'/api/dnscryptproxy/cloak/setCloak/', - 'add':'/api/dnscryptproxy/cloak/addCloak/', - 'del':'/api/dnscryptproxy/cloak/delCloak/', - 'toggle':'/api/dnscryptproxy/cloak/toggleCloak/' + { 'search':'/api/dnscryptproxy/cloak/search_cloak', + 'get':'/api/dnscryptproxy/cloak/get_cloak/', + 'set':'/api/dnscryptproxy/cloak/set_cloak/', + 'add':'/api/dnscryptproxy/cloak/add_cloak/', + 'del':'/api/dnscryptproxy/cloak/del_cloak/', + 'toggle':'/api/dnscryptproxy/cloak/toggle_cloak/' } ); $("#grid-whitelists").UIBootgrid( - { 'search':'/api/dnscryptproxy/whitelist/searchWhitelist', - 'get':'/api/dnscryptproxy/whitelist/getWhitelist/', - 'set':'/api/dnscryptproxy/whitelist/setWhitelist/', - 'add':'/api/dnscryptproxy/whitelist/addWhitelist/', - 'del':'/api/dnscryptproxy/whitelist/delWhitelist/', - 'toggle':'/api/dnscryptproxy/whitelist/toggleWhitelist/' + { 'search':'/api/dnscryptproxy/whitelist/search_whitelist', + 'get':'/api/dnscryptproxy/whitelist/get_whitelist/', + 'set':'/api/dnscryptproxy/whitelist/set_whitelist/', + 'add':'/api/dnscryptproxy/whitelist/add_whitelist/', + 'del':'/api/dnscryptproxy/whitelist/del_whitelist/', + 'toggle':'/api/dnscryptproxy/whitelist/toggle_whitelist/' } ); $("#grid-servers").UIBootgrid( - { 'search':'/api/dnscryptproxy/server/searchServer', - 'get':'/api/dnscryptproxy/server/getServer/', - 'set':'/api/dnscryptproxy/server/setServer/', - 'add':'/api/dnscryptproxy/server/addServer/', - 'del':'/api/dnscryptproxy/server/delServer/', - 'toggle':'/api/dnscryptproxy/server/toggleServer/' + { 'search':'/api/dnscryptproxy/server/search_server', + 'get':'/api/dnscryptproxy/server/get_server/', + 'set':'/api/dnscryptproxy/server/set_server/', + 'add':'/api/dnscryptproxy/server/add_server/', + 'del':'/api/dnscryptproxy/server/del_server/', + 'toggle':'/api/dnscryptproxy/server/toggle_server/' } ); diff --git a/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt-proxy.toml b/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt-proxy.toml index ce67f33cd..84d98ff08 100644 --- a/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt-proxy.toml +++ b/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt-proxy.toml @@ -40,6 +40,12 @@ doh_servers = true doh_servers = false {% endif %} +{% if helpers.exists('OPNsense.dnscryptproxy.general.odoh_servers') and OPNsense.dnscryptproxy.general.odoh_servers == '1' %} +odoh_servers = true +{% else %} +odoh_servers = false +{% endif %} + {% if helpers.exists('OPNsense.dnscryptproxy.general.require_dnssec') and OPNsense.dnscryptproxy.general.require_dnssec == '1' %} require_dnssec = true {% else %} @@ -146,7 +152,7 @@ cache = false [sources] [sources.'public-resolvers'] - urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/public-resolvers.md'] cache_file = 'public-resolvers.md' minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' refresh_delay = 72 @@ -155,12 +161,30 @@ cache = false ## Anonymized DNS relays [sources.'relays'] - urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://download.dnscrypt.net/resolvers-list/v3/relays.md'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/relays.md'] cache_file = 'relays.md' minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' refresh_delay = 72 prefix = '' + ## Oblivious DoH servers + + [sources.'odoh-servers'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-servers.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-servers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-servers.md'] + cache_file = 'odoh-servers.md' + minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + refresh_delay = 72 + prefix = '' + + ## Oblivious DoH relays + + [sources.'odoh-relays'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-relays.md'] + cache_file = 'odoh-relays.md' + minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + refresh_delay = 72 + prefix = '' + [anonymized_dns] {% if helpers.exists('OPNsense.dnscryptproxy.general.relaylist') and OPNsense.dnscryptproxy.general.relaylist != '' %} diff --git a/dns/rfc2136/Makefile b/dns/rfc2136/Makefile index 13df9955f..37d29c8e9 100644 --- a/dns/rfc2136/Makefile +++ b/dns/rfc2136/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= rfc2136 PLUGIN_VERSION= 1.9 -PLUGIN_REVISION= 3 +PLUGIN_REVISION= 4 PLUGIN_COMMENT= RFC-2136 Support PLUGIN_MAINTAINER= franco@opnsense.org PLUGIN_DEPENDS= bind-tools diff --git a/dns/rfc2136/src/etc/inc/plugins.inc.d/rfc2136.inc b/dns/rfc2136/src/etc/inc/plugins.inc.d/rfc2136.inc index 349f07dde..55c84297a 100644 --- a/dns/rfc2136/src/etc/inc/plugins.inc.d/rfc2136.inc +++ b/dns/rfc2136/src/etc/inc/plugins.inc.d/rfc2136.inc @@ -198,16 +198,15 @@ function rfc2136_configure_do($verbose = false, $int = null, $updatehost = '', $ $upinst .= "\n"; /* mind that trailing newline! */ if ($need_update) { - @file_put_contents("/var/etc/nsupdatecmds{$i}", $upinst); - unset($upinst); - /* invoke nsupdate */ - $cmd = "/usr/local/bin/nsupdate -k {$keyfile}"; + $cmds = "/var/etc/nsupdatecmds{$i}"; + file_safe($cmds, $upinst); + $args = []; + $args[] = exec_safe('-k %s', $keyfile); if (isset($dnsupdate['usetcp'])) { - $cmd .= " -v"; + $args[] = '-v'; } - $cmd .= " /var/etc/nsupdatecmds{$i}"; - mwexec_bg($cmd); - unset($cmd); + $args[] = exec_safe('%s', $cmds); + mwexecf_bg('/usr/local/bin/nsupdate ' . join(' ', $args)); } } @@ -222,7 +221,7 @@ function get_rfc2136_ip_address($int, $ipver = 4) return 'down'; } - if ($ipver != 6 && is_private_ip($ip_address)) { + if ($ipver != 6 && is_private_ipv4($ip_address)) { $ip_ch = curl_init('http://checkip.dyndns.org'); curl_setopt($ip_ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ip_ch, CURLOPT_INTERFACE, $ip_address); diff --git a/dns/rfc2136/src/www/widgets/include/rfc2136.inc b/dns/rfc2136/src/www/widgets/include/rfc2136.inc deleted file mode 100644 index c143db6dd..000000000 --- a/dns/rfc2136/src/www/widgets/include/rfc2136.inc +++ /dev/null @@ -1,4 +0,0 @@ - - * Copyright (C) 2014-2016 Deciso B.V. - * Copyright (C) 2008 Ermal Luçi - * Copyright (C) 2013 Stanley P. Miller \ stan-qaz - * 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. - */ - -require_once("guiconfig.inc"); -require_once("widgets/include/rfc2136.inc"); -require_once("interfaces.inc"); -require_once("plugins.inc.d/rfc2136.inc"); - -$a_rfc2136 = &config_read_array('dnsupdates', 'dnsupdate'); - -if (!empty($_REQUEST['getrfc2136status'])) { - $first_entry = true; - foreach ($a_rfc2136 as $rfc2136) { - if ($first_entry) { - $first_entry = false; - } else { - // Put a vertical bar delimiter between the echoed HTML for each entry processed. - echo '|'; - } - - $filename = rfc2136_cache_file($rfc2136, 4); - $fdata = ''; - if (!empty($rfc2136['enable']) && (empty($rfc2136['recordtype']) || $rfc2136['recordtype'] == 'A') && file_exists($filename)) { - $ipaddr = get_rfc2136_ip_address($rfc2136['interface'], 4); - $fdata = @file_get_contents($filename); - } - - $filename_v6 = rfc2136_cache_file($rfc2136, 6); - $fdata6 = ''; - if (!empty($rfc2136['enable']) && (empty($rfc2136['recordtype']) || $rfc2136['recordtype'] == 'AAAA') && file_exists($filename_v6)) { - $ipv6addr = get_rfc2136_ip_address($rfc2136['interface'], 6); - $fdata6 = @file_get_contents($filename_v6); - } - - if (!empty($fdata)) { - $cached_ip_s = explode('|', $fdata); - $cached_ip = $cached_ip_s[0]; - echo sprintf( - 'IPv4: %s', - $ipaddr != $cached_ip ? 'red' : 'green', - htmlspecialchars($cached_ip) - ); - } else { - echo 'IPv4: ' . gettext('N/A'); - } - - echo '
    '; - - if (!empty($fdata6)) { - $cached_ipv6_s = explode('|', $fdata6); - $cached_ipv6 = $cached_ipv6_s[0]; - echo sprintf( - 'IPv6: %s', - $ipv6addr != $cached_ipv6 ? 'red' : 'green', - htmlspecialchars($cached_ipv6) - ); - } else { - echo 'IPv6: ' . gettext('N/A'); - } - } - exit; -} - -?> - - - - - - - - - - - - $rfc2136) :?> - - - - - - - - -
    > - $ifdesc) { - if ($rfc2136['interface'] == $if) { - echo "{$ifdesc}"; - break; - } - }?> - > - - > - - > -
    - -
    -
    - diff --git a/emulators/qemu-guest-agent/src/opnsense/mvc/app/models/OPNsense/QemuGuestAgent/QemuGuestAgent.xml b/emulators/qemu-guest-agent/src/opnsense/mvc/app/models/OPNsense/QemuGuestAgent/QemuGuestAgent.xml index 8c037740b..59b056bbc 100644 --- a/emulators/qemu-guest-agent/src/opnsense/mvc/app/models/OPNsense/QemuGuestAgent/QemuGuestAgent.xml +++ b/emulators/qemu-guest-agent/src/opnsense/mvc/app/models/OPNsense/QemuGuestAgent/QemuGuestAgent.xml @@ -5,16 +5,11 @@ - 1 + 1 Y - - 0 - N - + - N - Y Y diff --git a/ftp/tftp/src/opnsense/mvc/app/models/OPNsense/Tftp/General.xml b/ftp/tftp/src/opnsense/mvc/app/models/OPNsense/Tftp/General.xml index 42284290b..68a2ba78a 100644 --- a/ftp/tftp/src/opnsense/mvc/app/models/OPNsense/Tftp/General.xml +++ b/ftp/tftp/src/opnsense/mvc/app/models/OPNsense/Tftp/General.xml @@ -4,11 +4,11 @@ 0.0.1 - 0 + 0 Y - 127.0.0.1 + 127.0.0.1 Y diff --git a/mail/postfix/Makefile b/mail/postfix/Makefile index f0c2159c3..5feb4c74f 100644 --- a/mail/postfix/Makefile +++ b/mail/postfix/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= postfix -PLUGIN_VERSION= 1.23 -PLUGIN_REVISION= 4 +PLUGIN_VERSION= 1.24 PLUGIN_COMMENT= SMTP mail relay PLUGIN_DEPENDS= postfix PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/mail/postfix/pkg-descr b/mail/postfix/pkg-descr index 6bce690ce..f9756bc69 100644 --- a/mail/postfix/pkg-descr +++ b/mail/postfix/pkg-descr @@ -6,6 +6,10 @@ is completely different. Plugin Changelog ================ +1.24 + +* Disable broken, insecure, legacy NTLM authentication (contributed by Alfred Egger) + 1.23 * Add support for Opportunistic DANE as SMTP client security level diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Address.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Address.xml index ffaf8c949..73baf4a49 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Address.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Address.xml @@ -6,7 +6,7 @@
    - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Antispam.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Antispam.xml index fdb3cac7f..037907446 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Antispam.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Antispam.xml @@ -4,11 +4,11 @@ 1.0.2 - 0 + 0 Y - accept + accept Y accept diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Domain.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Domain.xml index f74d697d7..a4af4ed77 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Domain.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Domain.xml @@ -6,17 +6,17 @@ - 1 + 1 Y - + Y - + N - /^([0-9a-zA-Z.:\-\[\]]){1,64}$/u + /^([0-9a-zA-Z.:\-\[\]]){1,64}$/u Only 64 of the following characters are allowed: 0-9a-zA-Z.:-[] diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/General.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/General.xml index 5a7c0c41a..a78a6b5b8 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/General.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/General.xml @@ -4,31 +4,31 @@ 1.2.7 - 0 + 0 Y - + N - + N - + N - all + all Y - 25 + 25 Y - all + all Y All @@ -45,24 +45,24 @@ ipv6 - 127.0.0.0/8,[::ffff:127.0.0.0]/104,[::1]/128 + 127.0.0.0/8,[::ffff:127.0.0.0]/104,[::1]/128 Y - + N - 51200000 + 51200000 Y N - /^([0-9a-z\.\-\_]{1,128})(,[0-9a-z\.\-\_]{1,128})*$/ui + /^([0-9a-z\.\-\_]{1,128})(,[0-9a-z\.\-\_]{1,128})*$/ui Only up to 128 of the following characters are allowed: 0-9a-zA-Z.-_ - intermediate + intermediate Y Modern @@ -71,7 +71,7 @@ - intermediate + intermediate Y Modern @@ -80,7 +80,7 @@ - 0 + 0 Y @@ -92,7 +92,7 @@ N - may + may Y none @@ -103,91 +103,91 @@ N - /^([0-9a-zA-Z.:\-\[\]]){1,64}$/u + /^([0-9a-zA-Z.:\-\[\]]){1,64}$/u Only 64 of the following characters are allowed: 0-9a-zA-Z.:-[] - 0 + 0 Y - + N - + N - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 0 + 0 Y - 0 + 0 N 0 24 diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Headerchecks.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Headerchecks.xml index 8e8b48377..684656250 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Headerchecks.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Headerchecks.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipient.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipient.xml index 40b5c48b4..5782e4a53 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipient.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipient.xml @@ -6,7 +6,7 @@ - 1 + 1 Y
    diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipientbcc.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipientbcc.xml index f763871c3..1adf18035 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipientbcc.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipientbcc.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sender.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sender.xml index ad250f802..6408af54d 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sender.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sender.xml @@ -6,7 +6,7 @@ - 1 + 1 Y
    diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Senderbcc.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Senderbcc.xml index 7f2610a92..4d696bffd 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Senderbcc.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Senderbcc.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sendercanonical.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sendercanonical.xml index 46b342438..0662d4d1f 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sendercanonical.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sendercanonical.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/address.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/address.volt index c6bc57777..7f1695ee1 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/address.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/address.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-addresses").UIBootgrid( - { 'search':'/api/postfix/address/searchAddress', - 'get':'/api/postfix/address/getAddress/', - 'set':'/api/postfix/address/setAddress/', - 'add':'/api/postfix/address/addAddress/', - 'del':'/api/postfix/address/delAddress/', - 'toggle':'/api/postfix/address/toggleAddress/' + { 'search':'/api/postfix/address/search_address', + 'get':'/api/postfix/address/get_address/', + 'set':'/api/postfix/address/set_address/', + 'add':'/api/postfix/address/add_address/', + 'del':'/api/postfix/address/del_address/', + 'toggle':'/api/postfix/address/toggle_address/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/domain.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/domain.volt index bf7e0b1af..a98dd0655 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/domain.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/domain.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-domains").UIBootgrid( - { 'search':'/api/postfix/domain/searchDomain', - 'get':'/api/postfix/domain/getDomain/', - 'set':'/api/postfix/domain/setDomain/', - 'add':'/api/postfix/domain/addDomain/', - 'del':'/api/postfix/domain/delDomain/', - 'toggle':'/api/postfix/domain/toggleDomain/' + { 'search':'/api/postfix/domain/search_domain', + 'get':'/api/postfix/domain/get_domain/', + 'set':'/api/postfix/domain/set_domain/', + 'add':'/api/postfix/domain/add_domain/', + 'del':'/api/postfix/domain/del_domain/', + 'toggle':'/api/postfix/domain/toggle_domain/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/headerchecks.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/headerchecks.volt index c33b317d4..3cbd1be08 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/headerchecks.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/headerchecks.volt @@ -34,12 +34,12 @@ *************************************************************************************************************/ $("#grid-headerchecks").UIBootgrid( - { 'search':'/api/postfix/headerchecks/searchHeaderchecks', - 'get':'/api/postfix/headerchecks/getHeadercheck/', - 'set':'/api/postfix/headerchecks/setHeadercheck/', - 'add':'/api/postfix/headerchecks/addHeadercheck/', - 'del':'/api/postfix/headerchecks/delHeadercheck/', - 'toggle':'/api/postfix/headerchecks/toggleHeadercheck/' + { 'search':'/api/postfix/headerchecks/search_headerchecks', + 'get':'/api/postfix/headerchecks/get_headercheck/', + 'set':'/api/postfix/headerchecks/set_headercheck/', + 'add':'/api/postfix/headerchecks/add_headercheck/', + 'del':'/api/postfix/headerchecks/del_headercheck/', + 'toggle':'/api/postfix/headerchecks/toggle_headercheck/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipient.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipient.volt index ed1e62be1..e1bf839c7 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipient.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipient.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-recipients").UIBootgrid( - { 'search':'/api/postfix/recipient/searchRecipient', - 'get':'/api/postfix/recipient/getRecipient/', - 'set':'/api/postfix/recipient/setRecipient/', - 'add':'/api/postfix/recipient/addRecipient/', - 'del':'/api/postfix/recipient/delRecipient/', - 'toggle':'/api/postfix/recipient/toggleRecipient/' + { 'search':'/api/postfix/recipient/search_recipient', + 'get':'/api/postfix/recipient/get_recipient/', + 'set':'/api/postfix/recipient/set_recipient/', + 'add':'/api/postfix/recipient/add_recipient/', + 'del':'/api/postfix/recipient/del_recipient/', + 'toggle':'/api/postfix/recipient/toggle_recipient/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipientbcc.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipientbcc.volt index 39bd7edc7..856734592 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipientbcc.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipientbcc.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-recipientbccs").UIBootgrid( - { 'search':'/api/postfix/recipientbcc/searchRecipientbcc', - 'get':'/api/postfix/recipientbcc/getRecipientbcc/', - 'set':'/api/postfix/recipientbcc/setRecipientbcc/', - 'add':'/api/postfix/recipientbcc/addRecipientbcc/', - 'del':'/api/postfix/recipientbcc/delRecipientbcc/', - 'toggle':'/api/postfix/recipientbcc/toggleRecipientbcc/' + { 'search':'/api/postfix/recipientbcc/search_recipientbcc', + 'get':'/api/postfix/recipientbcc/get_recipientbcc/', + 'set':'/api/postfix/recipientbcc/set_recipientbcc/', + 'add':'/api/postfix/recipientbcc/add_recipientbcc/', + 'del':'/api/postfix/recipientbcc/del_recipientbcc/', + 'toggle':'/api/postfix/recipientbcc/toggle_recipientbcc/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sender.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sender.volt index 078e6d386..217ce8186 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sender.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sender.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-senders").UIBootgrid( - { 'search':'/api/postfix/sender/searchSender', - 'get':'/api/postfix/sender/getSender/', - 'set':'/api/postfix/sender/setSender/', - 'add':'/api/postfix/sender/addSender/', - 'del':'/api/postfix/sender/delSender/', - 'toggle':'/api/postfix/sender/toggleSender/' + { 'search':'/api/postfix/sender/search_sender', + 'get':'/api/postfix/sender/get_sender/', + 'set':'/api/postfix/sender/set_sender/', + 'add':'/api/postfix/sender/add_sender/', + 'del':'/api/postfix/sender/del_sender/', + 'toggle':'/api/postfix/sender/toggle_sender/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/senderbcc.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/senderbcc.volt index 7b22a077b..96f324696 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/senderbcc.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/senderbcc.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-senderbccs").UIBootgrid( - { 'search':'/api/postfix/senderbcc/searchSenderbcc', - 'get':'/api/postfix/senderbcc/getSenderbcc/', - 'set':'/api/postfix/senderbcc/setSenderbcc/', - 'add':'/api/postfix/senderbcc/addSenderbcc/', - 'del':'/api/postfix/senderbcc/delSenderbcc/', - 'toggle':'/api/postfix/senderbcc/toggleSenderbcc/' + { 'search':'/api/postfix/senderbcc/search_senderbcc', + 'get':'/api/postfix/senderbcc/get_senderbcc/', + 'set':'/api/postfix/senderbcc/set_senderbcc/', + 'add':'/api/postfix/senderbcc/add_senderbcc/', + 'del':'/api/postfix/senderbcc/del_senderbcc/', + 'toggle':'/api/postfix/senderbcc/toggle_senderbcc/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sendercanonical.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sendercanonical.volt index 5324769fb..85848ec9a 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sendercanonical.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sendercanonical.volt @@ -36,12 +36,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-sendercanonicals").UIBootgrid( - { 'search':'/api/postfix/sendercanonical/searchSendercanonical', - 'get':'/api/postfix/sendercanonical/getSendercanonical/', - 'set':'/api/postfix/sendercanonical/setSendercanonical/', - 'add':'/api/postfix/sendercanonical/addSendercanonical/', - 'del':'/api/postfix/sendercanonical/delSendercanonical/', - 'toggle':'/api/postfix/sendercanonical/toggleSendercanonical/' + { 'search':'/api/postfix/sendercanonical/search_sendercanonical', + 'get':'/api/postfix/sendercanonical/get_sendercanonical/', + 'set':'/api/postfix/sendercanonical/set_sendercanonical/', + 'add':'/api/postfix/sendercanonical/add_sendercanonical/', + 'del':'/api/postfix/sendercanonical/del_sendercanonical/', + 'toggle':'/api/postfix/sendercanonical/toggle_sendercanonical/' } ); diff --git a/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/main.cf b/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/main.cf index 2d462bb42..49419f5d1 100644 --- a/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/main.cf +++ b/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/main.cf @@ -157,7 +157,7 @@ relayhost = {{ OPNsense.postfix.general.relayhost }} smtp_sasl_auth_enable = yes smtp_sasl_password_maps = hash:/usr/local/etc/postfix/smtp_auth smtp_sasl_security_options = -smtp_sasl_mechanism_filter = !gssapi, !external, static:all +smtp_sasl_mechanism_filter = !gssapi, !ntlm, !external, static:all {% endif %} {% if helpers.exists('OPNsense.postfix.general.permit_sasl_authenticated') and OPNsense.postfix.general.permit_sasl_authenticated == '1' %} diff --git a/mail/rspamd/src/opnsense/mvc/app/models/OPNsense/Rspamd/RSpamd.xml b/mail/rspamd/src/opnsense/mvc/app/models/OPNsense/Rspamd/RSpamd.xml index 66453a0fe..5bd77ea2f 100644 --- a/mail/rspamd/src/opnsense/mvc/app/models/OPNsense/Rspamd/RSpamd.xml +++ b/mail/rspamd/src/opnsense/mvc/app/models/OPNsense/Rspamd/RSpamd.xml @@ -5,15 +5,15 @@ - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y @@ -120,47 +120,47 @@ N - 200 + 200 Y 1 100000 Choose a value between 1 and 100000. - 127.0.0.1 + 127.0.0.1 Y , - Y + Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - /[a-z0-9\.\-_@,]+/i + /[a-z0-9\.\-_@,]+/i N @@ -182,18 +182,18 @@ N 32 A valid IPv4 mask must be between 1 and 32 bits. - 19 + 19 1 N 128 - 64 + 64 A valid IPv6 mask must be between 1 and 128 bits. 64 bits are recommended as this is the recommended subnet size in IPv6. N - /^([a-fA-F0-9\.:\[\]\/]*?,)*([a-fA-F0-9\.:\[\]\/]*)$/ + /^([a-fA-F0-9\.:\[\]\/]*?,)*([a-fA-F0-9\.:\[\]\/]*)$/ @@ -214,44 +214,44 @@ A valid time jitter must be set. - 0 + 0 Y - 0 + 0 Y - 1 + 1 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 1 + 1 Y - 1 + 1 Y - 0 + 0 Y - header + header Y
    Header
    @@ -259,43 +259,43 @@
    - 1 + 1 Y - 0 + 0 Y 1 N - 86400 + 86400 A valid cache expiration must be set. - 0 + 0 Y - + N - 0 + 0 Y - 0 + 0 Y - + N @@ -316,7 +316,7 @@ The time must be a positive integer. - m + m Y Seconds @@ -337,7 +337,7 @@ The time must be a positive integer. - m + m Y Seconds @@ -358,7 +358,7 @@ The time must be a positive integer. - m + m Y Seconds @@ -379,7 +379,7 @@ The time must be a positive integer. - m + m Y Seconds @@ -400,7 +400,7 @@ The time must be a positive integer. - m + m Y Seconds @@ -421,7 +421,7 @@ The time must be a positive integer. - m + m Y Seconds @@ -431,26 +431,26 @@ - postmaster,mailer-daemon + postmaster,mailer-daemon 1 Y - 20 + 20 - 0 + 0 Y - 0 + 0 Y - 1 + 1 Y @@ -462,7 +462,7 @@ 1 N - 2 + 2 A valid cache size in kilobytes must be set. @@ -474,16 +474,16 @@ - 1 + 1 Y - 1 + 1 Y 1 - 20000000 + 20000000 N A valid maximum size in bytes must be set. @@ -504,7 +504,7 @@ N - exe,dll,scr,com,cmd,js,bat,vbs,ps1,bat,cpl,lnk,msi,msp,reg + exe,dll,scr,com,cmd,js,bat,vbs,ps1,bat,cpl,lnk,msi,msp,reg N diff --git a/misc/theme-advanced/Makefile b/misc/theme-advanced/Makefile index d8fabc971..2e92be9e1 100644 --- a/misc/theme-advanced/Makefile +++ b/misc/theme-advanced/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= theme-advanced -PLUGIN_VERSION= 1.0 -PLUGIN_COMMENT= OPNsense theme based on AdvancedTomato GUI +PLUGIN_VERSION= 1.1 +PLUGIN_COMMENT= Theme based on AdvancedTomato GUI PLUGIN_MAINTAINER= jacky@prahec.com PLUGIN_WWW= https://prahec.com/ PLUGIN_NO_ABI= yes diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_tables.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_tables.scss index 2e5f9a51d..7f87ca4c1 100644 --- a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_tables.scss +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_tables.scss @@ -13,7 +13,6 @@ th { // Baseline styles - .table { width: 100%; max-width: 100%; diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/main.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/main.scss index 0498b12ce..3dd6c75aa 100644 --- a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/main.scss +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/main.scss @@ -149,10 +149,6 @@ body { touch-action: manipulation; } -.widget-sort-handle { - touch-action: none; -} - .page-head { position: fixed; z-index: map-get($zindex, header); @@ -198,7 +194,7 @@ body { font-size: 12px; font-weight: 400; display: inline-block; - content: "AdvancedOPN Theme"; + content: "Advanced Theme"; color: #fafafa; } @@ -1090,11 +1086,6 @@ select { appearance: none; } -#grid-log th[data-column-id="__timestamp__"], -#filter-log-entries th[data-column-id="__timestamp__"] { - min-width: 3.5em; -} - .table-responsive { @media screen and (max-width: $screen-xs-max) { margin-bottom: 0; @@ -1268,3 +1259,29 @@ div[data-for*="help_for"] { margin-bottom: -4px; } } + +/** +* HACKS +*/ +.widget-sort-handle { + touch-action: none; +} + +.bootgrid-table { + table-layout: auto !important; +} + +table#tunable.bootgrid-table td { + white-space: normal !important; +} + +@media (min-width: $screen-lg-desktop) { + table#grid-leases tr td:nth-child(3) { + width: 15% !important; + } +} + +#grid-log th[data-column-id="__timestamp__"], +#filter-log-entries th[data-column-id="__timestamp__"] { + min-width: 3.5em; +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/opnsense-bootgrid.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/opnsense-bootgrid.scss new file mode 100644 index 000000000..13d4509a6 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/opnsense-bootgrid.scss @@ -0,0 +1,488 @@ +$colors: ( + default: #536270, + primary: #51aded, + navbg: #263238, + lightgrey: #f7f7f7, + lightergrey: #fbfbfb, + darkgrey: #3c3c3b, + bordergrey: #eaeaea, +); + +@import "bootstrap/variables"; + +/** +* Customized +*/ +@mixin button-variant($color, $background, $border) { + color: $color; + background-color: $background; + border-color: $border; + + &:hover, + &:focus, + &:active, + &.active, + .open > &.dropdown-toggle, + :not(disabled):hover { + color: $color !important; + background-color: darken($background, 10%) !important; + border-color: darken($border, 12%) !important; + } + &:active, + &.active, + .open > &.dropdown-toggle { + background-image: none; + } + &.disabled, + &[disabled], + fieldset[disabled] & { + &, + &:hover, + &:focus, + &:active, + &.active { + background-color: $background; + border-color: $border; + } + } + + .badge { + color: $background; + background-color: $color; + } +} + +// Button sizes +@mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) { + padding: $padding-vertical $padding-horizontal; + font-size: $font-size; + line-height: $line-height; + border-radius: $border-radius; +} + + +/** +* Main theming elements +*/ +.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover { + background-color: #FBFBFB; +} + +.tabulator .tabulator-headers { + height: 100% !important; /* make sure the border below appears */ +} + +/*------------*/ +/* theme: body */ +.tabulator .tabulator-tableholder .tabulator-table { + background-color: transparent; +} + +.tabulator-row { + background-color: transparent; + color: #373736; +} + +.tabulator-row.tabulator-row-odd { + background-color: #FBFBFB; +} + +.tabulator-row.tabulator-row-odd.tabulator-selectable:hover:not(.tabulator-selected) { + background-color: $table-bg-hover; +} + +.tabulator-row.tabulator-row-even.tabulator-selectable:hover:not(.tabulator-selected) { + background-color: $table-bg-hover; +} + +.tabulator .tabulator-tableholder { + background-color: transparent; +} + +.tabulator-row.tabulator-selected { + background-color: #e3e8f0; +} + +.tabulator-row.tabulator-selected:hover { + cursor: pointer; + background-color: #e3e8f0; +} + +.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg { + color: #373736; /* spinner icon */ +} + +/* Command column styles */ +.tabulator-col.tabulator-frozen.tabulator-frozen-right { +} + +.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right { +} + +/* Tabulator groupBy row styles */ +.tabulator-row.tabulator-group { + border: none; + pointer-events: none; +} + +.tabulator-row.tabulator-group span { + margin-left: 0; + color: #373736; +} + +.tabulator-row .badge.chip { + background-color: #31708f; + color: #FFF; + font-size: 10px; + padding: 2px 4px; + position: relative; + top: -1px; +} + +.tabulator-row.tabulator-group .tabulator-group-toggle, +.tabulator-row.tabulator-group .tabulator-group-toggle * { + pointer-events: auto; +} + +.tabulator-row.tabulator-group:has(> .tabulator-group-toggle:only-child) { + display: none; /* Trick to hide empty groupBy header rows */ +} + +/*------------*/ +/* theme: cells */ +.tabulator-row .tabulator-cell { + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.2; + vertical-align: top; +} + +/*-------------*/ +/* theme: footer */ +.tabulator .tabulator-footer { + border-top: none; + background-color: transparent; +} + +.tabulator-page-counter { + color: #373736; +} + +.tabulator .tabulator-footer .tabulator-paginator { + color: #373736; +} + +.tabulator .tabulator-footer .tabulator-page { + +} + +/*----------*/ +/* end of main theming elements */ +.tabulator-paginator { + flex: 0 !important; +} + +.tabulator-page-counter { + flex: 1; + text-align: right; +} + +.tabulator-col-sorter i { + display: flex; + justify-content: center; + align-items: center; +} + +.tabulator .tabulator-header .tabulator-col { + padding: 5px; +} + +.tabulator .tabulator-headers > :first-child { + padding-left: 10px; +} + +.opnsense-bootgrid-responsive { + white-space: wrap !important; + text-overflow: inherit !important; + overflow: visible !important; + word-break: break-word !important; +} + +.opnsense-bootgrid-ellipsis { + overflow: hidden !important; + white-space: nowrap !important; + text-overflow: ellipsis !important; +} + +.tabulator-row > :first-child { + padding-left: 10px; +} + +.tabulator .tabulator-header .tabulator-col .tabulator-col-content { + padding: 0px; +} + +.tabulator-page-counter { + padding-right: 20px; +} + +.bootgrid-footer-commands { + width: 90px; + padding-left: 8px; +} + +.tabulator-tableholder::after { + content: ""; + display: block; + width: 1px; + height: 1px; + min-width: 100%; +} + +.tabulator .tabulator-footer .tabulator-paginator .tabulator-page:first-child { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; +} + +.tabulator .tabulator-footer .tabulator-paginator .tabulator-page:last-child { + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; +} + +/* border line corrections and hover/selection behavior */ +.tabulator-row:first-child > .tabulator-cell { + border-top: none; +} + +.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title { + padding: 0px; +} + +/* Row highlight behavior */ +@keyframes highlight { + 0% { + background: #ffff99; + } + 100% { + background: none; + } +} + +.highlight-bg { + animation: highlight 2s; +} + +/* Tabulator "loading" and "error" overrides */ +.tabulator .tabulator-alert { + background: none; +} + +.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg { + border: none; +} + +.tabulator .tabulator-alert .tabulator-alert-msg { + background: initial; + font-size: 18px; +} + +.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error { + border: none; +} + +.bootgrid-placeholder { + font-size: 15px; + text-align: center; + white-space: normal; + font-weight: 400; + margin: 10px; +} + +.bootgrid-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 99; +} + +.overlay > i { + font-size: 20px; + color: black; +} + +/** +* Main theming elements for tabulator +/* Base */ +.tabulator { + width: 100%; + font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; + font-size: 13px; + background: #fff; + border: 1px solid $table-border-color; /* .table-bordered */ + border-radius: 0; +} + +/* Header */ +.tabulator .tabulator-header { + background: #fff; + border-bottom: 1px solid $table-border-color; /* thead bottom border */ + border-top: 0; + border-left: 0; + border-right: 0; +} + +.tabulator .tabulator-header .tabulator-col { + background: transparent; + color: #333; + border-right: 1px solid $table-border-color; /* col separators */ +} + +.tabulator .tabulator-header .tabulator-col:last-child { + border-right: 0; +} + +/* Header cell inner (align/padding like Bootstrap) */ +.tabulator .tabulator-header .tabulator-col .tabulator-col-content { + padding: 10px 0 10px 20px; /* th padding */ + line-height: 1.428571429; +} + +/* Sort hover (subtle) */ +.tabulator .tabulator-header .tabulator-col.tabulator-sortable:hover { + background: #f5f5f5; +} + + +/* Body */ +.tabulator .tabulator-tableholder { + background: #fff; +} + +.tabulator-row { + background: #fff; + color: #333; + border-top: 1px solid $table-border-color; /* row top border like Bootstrap */ +} + +/* Cells */ +.tabulator-row .tabulator-cell { + padding: 10px 0 10px 20px; /* td padding */ + line-height: 1.428571429; + vertical-align: top; + border-right: 1px solid $table-border-color; /* col separators */ + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left, +.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left, +.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left, +.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left, +.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right, +.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-right { + border-color: $table-border-color; + border-width: 1px; +} + +.tabulator-row .tabulator-cell:last-child { + border-right: 0; +} + +.tabulator-header { + .tabulator-row-header input, + .tabulator-cell input { + margin-left: -24px; + } +} + +.tabulator-table { + .tabulator-row-header input, + .tabulator-cell input { + margin-left: -18px; + } +} + +/* Striped (.table-striped) */ +.tabulator.tabulator-striped .tabulator-row:nth-child(odd) { + background: #fbfbfb; +} + +/* Hover (.table-hover) */ +.tabulator.tabulator-hover .tabulator-row:hover { + background: $table-bg-hover !important; +} + +/* Active state (matches .active bg) */ +.tabulator-row.tabulator-selected { + background: $table-bg-hover !important; +} + +.tabulator-row.tabulator-selected:hover { + background: $table-bg-hover !important; +} + +/* Footer (pagination) */ +.tabulator .tabulator-footer { + background: #fff; + border-top: 1px solid $table-border-color; + color: #555; +} + +.tabulator .tabulator-footer .tabulator-page { + border: 1px solid $table-border-color; + background: #fff; + color: #333; + border-radius: 0; + margin: 0; + padding: 6px 12px; /* Bootstrap-like pager */ + + @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border); +} + +@media (hover: hover) and (pointer: fine) { + .tabulator .tabulator-footer .tabulator-page:not(disabled):hover { + } +} + +.tabulator-row.tabulator-row-even { + background-color: $table-bg +} + +.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title { + overflow: visible; +} + + +.tabulator-row.tabulator-selected { + background-color: #9abcea +} + +.tabulator .tabulator-alert { + align-items: center; + background: rgba(#fff, .5); + display: flex; + height: 100%; + left: 0; + position: absolute; + text-align: center; + top: 0; + width: 100%; + z-index: 100; +} + +/** + * jQuery Bootstrap Grid + */ +.bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item.dropdown-item-checkbox, +.bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item.dropdown-item-checkbox, +.bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item .dropdown-item-checkbox, +.bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item .dropdown-item-checkbox { + margin: 0 8px 4px -8px !important; +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/dashboard.css b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/dashboard.css index f140ce341..6b67121b1 100644 --- a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/dashboard.css +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/dashboard.css @@ -1 +1 @@ -:root{--chart-js-background-color: #f7e2d6;--chart-js-border-color: #f6f6f6;--chart-js-font-color: #4b4b64}.btn-pressed,.btn-pressed:hover{color:white;background-color:#0086d9}#save-grid{position:relative;display:inline-flex;align-items:center;justify-content:center;width:50px;height:33px;margin-right:10px;transition:opacity 0.3s ease}#save-btn-text,#icon-container{position:absolute}#icon-container{display:inline-flex;align-items:center;justify-content:center}#save-spinner,#save-check{transition:opacity 0.3s ease, transform 0.3s ease}.transition-spinner,transition-check{transition:opacity 0.3s ease, transform 0.3s ease}.grid-stack-item-content{text-align:center;border:none !important;border-radius:0.5em 0.5em 0.5em 0.5em;background-color:#fff;box-shadow:0 2px 4px rgba(40,40,50,0.15),0 0 1px rgba(40,40,50,0.35)}.widget-error{margin:50px;color:#721c24}.widget-content{position:relative;width:100%;height:100%;padding:1px;cursor:grab}.widget-header{font-size:14px;display:flex;align-items:center;justify-content:space-between;margin-top:0.5em;margin-right:1em;margin-left:1em}.widget-spinner{margin-top:20px}.fa-stack.small{font-size:0.5em}.close-handle,.edit-handle{padding:.2rem .5rem;cursor:pointer;text-align:right;vertical-align:middle}.close-handle>i,.edit-handle>i{font-size:1.2rem !important}.widget-header-left{display:flex;align-items:center;flex:1;justify-content:flex-start}.widget-command-container{display:flex;align-items:center;flex:1;justify-content:flex-end}.widget-title{display:flex;align-items:center;justify-content:center}.panel-divider{width:100%;height:8px;margin-bottom:10px;text-align:center}.panel-divider .line{display:none}td{word-break:break-all}.canvas-container{position:relative}.cpu-canvas-container{display:flex;flex-direction:column}.smoothie-container{width:100%}.smoothie-chart-tooltip{font-size:13px;z-index:1;padding-right:15px;padding-left:15px;pointer-events:none;color:white;border-radius:0.5em 0.5em 0.5em 0.5em;background:rgba(50,50,50,0.9)}.flex-container{display:flex;flex-wrap:nowrap;white-space:nowrap}.gateway-info{font-size:13px;margin:5px;padding:5px}.gateway-detail-container{display:none;margin:5px}.interface-info{display:flex;align-items:center;flex-wrap:wrap;height:100%}.nowrap{flex-wrap:nowrap}.gateway-graph{display:none}.flex-container>.gateway-graph{font-size:13px}.vertical-center-row{display:inline;height:100%}.interfaces-info{font-size:.8rem;margin:5px}.interface-descr{font-size:1em;margin-left:.8em;cursor:pointer;text-decoration:underline}.interfaces-detail-container{display:none;margin:5px}.d-flex{display:flex}.d-flex>.justify-content-start{justify-content:start}.d-flex>.justify-content-end{justify-content:end}#chartjs-toolip{z-index:20}.cpu-type{margin-top:10px;margin-bottom:10px}div{box-sizing:border-box}.flextable-container{display:block;width:95%;max-width:1200px;margin:2em auto}.flextable-header{display:flex;flex-flow:row wrap;padding:0.5em 0.5em;transition:0.5s;border-top:solid 1px rgba(0,119,217,0.15) !important}.flextable-row{display:flex;align-items:center;flex-flow:row wrap;padding:0.5em 0.5em;transition:0.5s;border-top:solid 1px #e8eaef !important}.flextable-header .flex-cell{font-weight:bold}.flextable-row:hover{transition:500ms;background:#f5f5f5}.flex-cell{padding:4px 0;text-align:left;word-break:break-word}.column{display:flex;flex-flow:column wrap;width:50%;padding:0}.column .flex-cell{display:flex;flex-flow:row wrap;width:100%;padding:4px 0;border:0;border-top:#e8eaef}.column .flex-cell:hover{transition:500ms;background:#f5f5f5}.flex-subcell{width:100%;text-align:left}.column .flex-cell:not(:last-child){border-bottom:solid 1px #e8eaef !important}.grid-header-container{display:grid;grid-template-columns:repeat(auto-fit, minmax(100px, 1fr))}.grid-row{display:grid;transition:0.5s;opacity:0.4;border-top:1px solid #eff3f8;background-color:#d6e5f7;grid-template-columns:repeat(auto-fit, minmax(100px, 1fr))}.grid-row:hover{transition:500ms;background:#f5f5f5 !important}.grid-header{font-weight:bold;border-top:1px solid #eff3f8}.grid-item{padding:4px;text-align:center}.ovpn-common-name{display:flex;align-items:center;justify-content:center} +:root{--chart-js-background-color: #f7e2d6;--chart-js-border-color: #f6f6f6;--chart-js-font-color: #4b4b64}.btn-pressed,.btn-pressed:hover{color:#fff;background-color:#0086d9}#save-grid{position:relative;display:inline-flex;align-items:center;justify-content:center;width:50px;height:33px;margin-right:10px;transition:opacity .3s ease}#save-btn-text,#icon-container{position:absolute}#icon-container{display:inline-flex;align-items:center;justify-content:center}#save-spinner,#save-check{transition:opacity .3s ease,transform .3s ease}.transition-spinner,transition-check{transition:opacity .3s ease,transform .3s ease}.grid-stack-item-content{text-align:center;border:none !important;border-radius:.5em .5em .5em .5em;background-color:#fff;box-shadow:0 2px 4px rgba(40,40,50,.15),0 0 1px rgba(40,40,50,.35)}.widget-error{margin:50px;color:#721c24}.widget-content{position:relative;width:100%;height:100%;padding:1px;cursor:grab}.widget-header{font-size:14px;display:flex;align-items:center;justify-content:space-between;margin-top:.5em;margin-right:1em;margin-left:1em}.widget-spinner{margin-top:20px}.fa-stack.small{font-size:.5em}.close-handle,.edit-handle{padding:.2rem .5rem;cursor:pointer;text-align:right;vertical-align:middle}.close-handle>i,.edit-handle>i{font-size:1.2rem !important}.widget-header-left{display:flex;align-items:center;flex:1;justify-content:flex-start}.widget-command-container{display:flex;align-items:center;flex:1;justify-content:flex-end}.widget-title{display:flex;align-items:center;justify-content:center}.panel-divider{width:100%;height:8px;margin-bottom:10px;text-align:center}.panel-divider .line{display:none}td{word-break:break-all}.canvas-container{position:relative}.cpu-canvas-container{display:flex;flex-direction:column}.smoothie-container{width:100%}.smoothie-chart-tooltip{font-size:13px;z-index:1;padding-right:15px;padding-left:15px;pointer-events:none;color:#fff;border-radius:.5em .5em .5em .5em;background:rgba(50,50,50,.9)}.flex-container{display:flex;flex-wrap:nowrap;white-space:nowrap}.gateway-info{font-size:13px;margin:5px;padding:5px}.gateway-detail-container{display:none;margin:5px}.interface-info{display:flex;align-items:center;flex-wrap:wrap;height:100%}.nowrap{flex-wrap:nowrap}.gateway-graph{display:none}.flex-container>.gateway-graph{font-size:13px}.vertical-center-row{display:inline;height:100%}.interfaces-info{font-size:.8rem;margin:5px}.interface-descr{font-size:1em;margin-left:.8em;cursor:pointer;text-decoration:underline}.interfaces-detail-container{display:none;margin:5px}.d-flex{display:flex}.d-flex>.justify-content-start{justify-content:start}.d-flex>.justify-content-end{justify-content:end}#chartjs-toolip{z-index:20}.cpu-type{margin-top:10px;margin-bottom:10px}div{box-sizing:border-box}.flextable-container{display:block;width:95%;max-width:1200px;margin:2em auto}.flextable-header{display:flex;flex-flow:row wrap;padding:.5em .5em;transition:.5s;border-top:solid 1px rgba(0,119,217,.15) !important}.flextable-row{display:flex;align-items:center;flex-flow:row wrap;padding:.5em .5em;transition:.5s;border-top:solid 1px #e8eaef !important}.flextable-header .flex-cell{font-weight:bold}.flextable-row:hover{transition:500ms;background:#f5f5f5}.flex-cell{padding:4px 0;text-align:left;word-break:break-word}.column{display:flex;flex-flow:column wrap;width:50%;padding:0}.column .flex-cell{display:flex;flex-flow:row wrap;width:100%;padding:4px 0;border:0;border-top:#e8eaef}.column .flex-cell:hover{transition:500ms;background:#f5f5f5}.flex-subcell{width:100%;text-align:left}.column .flex-cell:not(:last-child){border-bottom:solid 1px #e8eaef !important}.grid-header-container{display:grid;grid-template-columns:repeat(auto-fit, minmax(100px, 1fr))}.grid-row{display:grid;transition:.5s;opacity:.4;border-top:1px solid #eff3f8;background-color:#d6e5f7;grid-template-columns:repeat(auto-fit, minmax(100px, 1fr))}.grid-row:hover{transition:500ms;background:#f5f5f5 !important}.grid-header{font-weight:bold;border-top:1px solid #eff3f8}.grid-item{padding:4px;text-align:center}.ovpn-common-name{display:flex;align-items:center;justify-content:center}/*# sourceMappingURL=dashboard.css.map */ diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/main.css b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/main.css index 9333e6d6b..72b2b553f 100644 --- a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/main.css +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/main.css @@ -1 +1 @@ -.widgetdiv .content-box-head{overflow:hidden;min-height:25px !important;padding:8px !important;color:#3c3c3b !important;border-top-left-radius:3px;border-top-right-radius:3px;background:#e8e8e8 !important}.widgetdiv .content-box-head .btn-group .btn{color:#3c3c3b !important;border:0px;background:#e8e8e8 !important}.widgetdiv .content-box-head .btn-group .btn .glyphicon{color:#3c3c3b !important}.widgetdiv .content-box-head .list-inline li>h3{padding-top:4px}@font-face{font-family:"Inter";font-style:normal;font-weight:600;font-display:swap;src:url("/ui/themes/advanced/build/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Inter";font-style:normal;font-weight:500;font-display:swap;src:url("/ui/themes/advanced/build/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Inter";font-style:normal;font-weight:400;font-display:swap;src:url("/ui/themes/advanced/build/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}/*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:rgba(0,0,0,0)}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none !important;color:#000 !important;background:rgba(0,0,0,0) !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff !important}.navbar{display:none}.table td,.table th{background-color:#fff !important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}@font-face{font-family:"Glyphicons Halflings";src:url("../fonts/bootstrap/glyphicons-halflings-regular.eot");src:url("../fonts/bootstrap/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/bootstrap/glyphicons-halflings-regular.woff") format("woff"),url("../fonts/bootstrap/glyphicons-halflings-regular.ttf") format("truetype"),url("../fonts/bootstrap/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"*"}.glyphicon-plus:before{content:"+"}.glyphicon-euro:before{content:"€"}.glyphicon-minus:before{content:"−"}.glyphicon-cloud:before{content:"☁"}.glyphicon-envelope:before{content:"✉"}.glyphicon-pencil:before{content:"✏"}.glyphicon-glass:before{content:""}.glyphicon-music:before{content:""}.glyphicon-search:before{content:""}.glyphicon-heart:before{content:""}.glyphicon-star:before{content:""}.glyphicon-star-empty:before{content:""}.glyphicon-user:before{content:""}.glyphicon-film:before{content:""}.glyphicon-th-large:before{content:""}.glyphicon-th:before{content:""}.glyphicon-th-list:before{content:""}.glyphicon-ok:before{content:""}.glyphicon-remove:before{content:""}.glyphicon-zoom-in:before{content:""}.glyphicon-zoom-out:before{content:""}.glyphicon-off:before{content:""}.glyphicon-signal:before{content:""}.glyphicon-cog:before{content:""}.glyphicon-trash:before{content:""}.glyphicon-home:before{content:""}.glyphicon-file:before{content:""}.glyphicon-time:before{content:""}.glyphicon-road:before{content:""}.glyphicon-download-alt:before{content:""}.glyphicon-download:before{content:""}.glyphicon-upload:before{content:""}.glyphicon-inbox:before{content:""}.glyphicon-play-circle:before{content:""}.glyphicon-repeat:before{content:""}.glyphicon-refresh:before{content:""}.glyphicon-list-alt:before{content:""}.glyphicon-lock:before{content:""}.glyphicon-flag:before{content:""}.glyphicon-headphones:before{content:""}.glyphicon-volume-off:before{content:""}.glyphicon-volume-down:before{content:""}.glyphicon-volume-up:before{content:""}.glyphicon-qrcode:before{content:""}.glyphicon-barcode:before{content:""}.glyphicon-tag:before{content:""}.glyphicon-tags:before{content:""}.glyphicon-book:before{content:""}.glyphicon-bookmark:before{content:""}.glyphicon-print:before{content:""}.glyphicon-camera:before{content:""}.glyphicon-font:before{content:""}.glyphicon-bold:before{content:""}.glyphicon-italic:before{content:""}.glyphicon-text-height:before{content:""}.glyphicon-text-width:before{content:""}.glyphicon-align-left:before{content:""}.glyphicon-align-center:before{content:""}.glyphicon-align-right:before{content:""}.glyphicon-align-justify:before{content:""}.glyphicon-list:before{content:""}.glyphicon-indent-left:before{content:""}.glyphicon-indent-right:before{content:""}.glyphicon-facetime-video:before{content:""}.glyphicon-picture:before{content:""}.glyphicon-map-marker:before{content:""}.glyphicon-adjust:before{content:""}.glyphicon-tint:before{content:""}.glyphicon-edit:before{content:""}.glyphicon-share:before{content:""}.glyphicon-check:before{content:""}.glyphicon-move:before{content:""}.glyphicon-step-backward:before{content:""}.glyphicon-fast-backward:before{content:""}.glyphicon-backward:before{content:""}.glyphicon-play:before{content:""}.glyphicon-pause:before{content:""}.glyphicon-stop:before{content:""}.glyphicon-forward:before{content:""}.glyphicon-fast-forward:before{content:""}.glyphicon-step-forward:before{content:""}.glyphicon-eject:before{content:""}.glyphicon-chevron-left:before{content:""}.glyphicon-chevron-right:before{content:""}.glyphicon-plus-sign:before{content:""}.glyphicon-minus-sign:before{content:""}.glyphicon-remove-sign:before{content:""}.glyphicon-ok-sign:before{content:""}.glyphicon-question-sign:before{content:""}.glyphicon-info-sign:before{content:""}.glyphicon-screenshot:before{content:""}.glyphicon-remove-circle:before{content:""}.glyphicon-ok-circle:before{content:""}.glyphicon-ban-circle:before{content:""}.glyphicon-arrow-left:before{content:""}.glyphicon-arrow-right:before{content:""}.glyphicon-arrow-up:before{content:""}.glyphicon-arrow-down:before{content:""}.glyphicon-share-alt:before{content:""}.glyphicon-resize-full:before{content:""}.glyphicon-resize-small:before{content:""}.glyphicon-exclamation-sign:before{content:""}.glyphicon-gift:before{content:""}.glyphicon-leaf:before{content:""}.glyphicon-fire:before{content:""}.glyphicon-eye-open:before{content:""}.glyphicon-eye-close:before{content:""}.glyphicon-warning-sign:before{content:""}.glyphicon-plane:before{content:""}.glyphicon-calendar:before{content:""}.glyphicon-random:before{content:""}.glyphicon-comment:before{content:""}.glyphicon-magnet:before{content:""}.glyphicon-chevron-up:before{content:""}.glyphicon-chevron-down:before{content:""}.glyphicon-retweet:before{content:""}.glyphicon-shopping-cart:before{content:""}.glyphicon-folder-close:before{content:""}.glyphicon-folder-open:before{content:""}.glyphicon-resize-vertical:before{content:""}.glyphicon-resize-horizontal:before{content:""}.glyphicon-hdd:before{content:""}.glyphicon-bullhorn:before{content:""}.glyphicon-bell:before{content:""}.glyphicon-certificate:before{content:""}.glyphicon-thumbs-up:before{content:""}.glyphicon-thumbs-down:before{content:""}.glyphicon-hand-right:before{content:""}.glyphicon-hand-left:before{content:""}.glyphicon-hand-up:before{content:""}.glyphicon-hand-down:before{content:""}.glyphicon-circle-arrow-right:before{content:""}.glyphicon-circle-arrow-left:before{content:""}.glyphicon-circle-arrow-up:before{content:""}.glyphicon-circle-arrow-down:before{content:""}.glyphicon-globe:before{content:""}.glyphicon-wrench:before{content:""}.glyphicon-tasks:before{content:""}.glyphicon-filter:before{content:""}.glyphicon-briefcase:before{content:""}.glyphicon-fullscreen:before{content:""}.glyphicon-dashboard:before{content:""}.glyphicon-paperclip:before{content:""}.glyphicon-heart-empty:before{content:""}.glyphicon-link:before{content:""}.glyphicon-phone:before{content:""}.glyphicon-pushpin:before{content:""}.glyphicon-usd:before{content:""}.glyphicon-gbp:before{content:""}.glyphicon-sort:before{content:""}.glyphicon-sort-by-alphabet:before{content:""}.glyphicon-sort-by-alphabet-alt:before{content:""}.glyphicon-sort-by-order:before{content:""}.glyphicon-sort-by-order-alt:before{content:""}.glyphicon-sort-by-attributes:before{content:""}.glyphicon-sort-by-attributes-alt:before{content:""}.glyphicon-unchecked:before{content:""}.glyphicon-expand:before{content:""}.glyphicon-collapse-down:before{content:""}.glyphicon-collapse-up:before{content:""}.glyphicon-log-in:before{content:""}.glyphicon-flash:before{content:""}.glyphicon-log-out:before{content:""}.glyphicon-new-window:before{content:""}.glyphicon-record:before{content:""}.glyphicon-save:before{content:""}.glyphicon-open:before{content:""}.glyphicon-saved:before{content:""}.glyphicon-import:before{content:""}.glyphicon-export:before{content:""}.glyphicon-send:before{content:""}.glyphicon-floppy-disk:before{content:""}.glyphicon-floppy-saved:before{content:""}.glyphicon-floppy-remove:before{content:""}.glyphicon-floppy-save:before{content:""}.glyphicon-floppy-open:before{content:""}.glyphicon-credit-card:before{content:""}.glyphicon-transfer:before{content:""}.glyphicon-cutlery:before{content:""}.glyphicon-header:before{content:""}.glyphicon-compressed:before{content:""}.glyphicon-earphone:before{content:""}.glyphicon-phone-alt:before{content:""}.glyphicon-tower:before{content:""}.glyphicon-stats:before{content:""}.glyphicon-sd-video:before{content:""}.glyphicon-hd-video:before{content:""}.glyphicon-subtitles:before{content:""}.glyphicon-sound-stereo:before{content:""}.glyphicon-sound-dolby:before{content:""}.glyphicon-sound-5-1:before{content:""}.glyphicon-sound-6-1:before{content:""}.glyphicon-sound-7-1:before{content:""}.glyphicon-copyright-mark:before{content:""}.glyphicon-registration-mark:before{content:""}.glyphicon-cloud-download:before{content:""}.glyphicon-cloud-upload:before{content:""}.glyphicon-tree-conifer:before{content:""}.glyphicon-tree-deciduous:before{content:""}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:1.428571429;color:#3c3c3b;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#51aded;text-decoration:none}a:hover,a:focus{color:#178adb;text-decoration:underline}a:focus{outline:none}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;width:100% \9 ;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:3px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;width:100% \9 ;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:18px;margin-bottom:18px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Inter";font-weight:500;line-height:1.4;color:inherit}h1 small,h1 .small,h2 small,h2 .small,h3 small,h3 .small,h4 small,h4 .small,h5 small,h5 .small,h6 small,h6 .small,.h1 small,.h1 .small,.h2 small,.h2 .small,.h3 small,.h3 .small,.h4 small,.h4 .small,.h5 small,.h5 .small,.h6 small,.h6 .small{font-weight:normal;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:18px;margin-bottom:9px}h1 small,h1 .small,.h1 small,.h1 .small,h2 small,h2 .small,.h2 small,.h2 .small,h3 small,h3 .small,.h3 small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:9px;margin-bottom:9px}h4 small,h4 .small,.h4 small,.h4 .small,h5 small,h5 .small,.h5 small,.h5 .small,h6 small,h6 .small,.h6 small,.h6 .small{font-size:75%}h1,.h1{font-size:22px}h2,.h2{font-size:14px}h3,.h3{font-size:13px}h4,.h4{font-size:17px}h5,.h5{font-size:13px}h6,.h6{font-size:12px}p{margin:0 0 9px}.lead{margin-bottom:18px;font-size:14px;font-weight:300;line-height:1.4}@media(min-width: 768px){.lead{font-size:19.5px}}small,.small{font-size:92%}cite{font-style:normal}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#51aded}a.text-primary:hover{color:#2397e8}.text-success{color:#7ebc59}a.text-success:hover{color:#65a141}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#f0ad4e}a.text-warning:hover{color:#ec971f}.text-danger{color:#ee451f}a.text-danger:hover{color:#cb320f}.bg-primary{color:#fff}.bg-primary{background-color:#51aded}a.bg-primary:hover{background-color:#2397e8}.bg-success{background-color:#7ebc59}a.bg-success:hover{background-color:#65a141}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#ee451f}a.bg-danger:hover{background-color:#cb320f}.page-header{padding-bottom:8px;margin:36px 0 18px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:9px}ul ul,ul ol,ol ul,ol ol{margin-bottom:0}.list-unstyled,.list-inline{padding-left:0;list-style:none}.list-inline{margin-left:-5px}.list-inline>li{float:left;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:18px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}.dl-horizontal dd:before,.dl-horizontal dd:after{content:" ";display:table}.dl-horizontal dd:after{clear:both}@media(min-width: 768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:9px 18px;margin:0 0 18px;font-size:16.25px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.428571429;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:"— "}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,.blockquote-reverse small:before,.blockquote-reverse .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before,blockquote.pull-right .small:before{content:""}.blockquote-reverse footer:after,.blockquote-reverse small:after,.blockquote-reverse .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after,blockquote.pull-right .small:after{content:" —"}blockquote:before,blockquote:after{content:""}address{margin-bottom:18px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:3px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{display:block;padding:calc((18px - 100%)/2);margin:0 0 9px;font-size:12px;line-height:1.428571429;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:3px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:rgba(0,0,0,0);border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:20px;padding-right:20px}.container:before,.container:after{content:" ";display:table}.container:after{clear:both}@media(min-width: 768px){.container{width:760px}}@media(min-width: 992px){.container{width:980px}}@media(min-width: 1200px){.container{width:1180px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:20px;padding-right:20px}.container-fluid:before,.container-fluid:after{content:" ";display:table}.container-fluid:after{clear:both}.row{margin-left:-20px;margin-right:-20px}.row:before,.row:after{content:" ";display:table}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:20px;padding-right:20px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-1{width:8.3333333333%}.col-xs-2{width:16.6666666667%}.col-xs-3{width:25%}.col-xs-4{width:33.3333333333%}.col-xs-5{width:41.6666666667%}.col-xs-6{width:50%}.col-xs-7{width:58.3333333333%}.col-xs-8{width:66.6666666667%}.col-xs-9{width:75%}.col-xs-10{width:83.3333333333%}.col-xs-11{width:91.6666666667%}.col-xs-12{width:100%}.col-xs-pull-0{right:auto}.col-xs-pull-1{right:8.3333333333%}.col-xs-pull-2{right:16.6666666667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.3333333333%}.col-xs-pull-5{right:41.6666666667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.3333333333%}.col-xs-pull-8{right:66.6666666667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.3333333333%}.col-xs-pull-11{right:91.6666666667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:auto}.col-xs-push-1{left:8.3333333333%}.col-xs-push-2{left:16.6666666667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.3333333333%}.col-xs-push-5{left:41.6666666667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.3333333333%}.col-xs-push-8{left:66.6666666667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.3333333333%}.col-xs-push-11{left:91.6666666667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0%}.col-xs-offset-1{margin-left:8.3333333333%}.col-xs-offset-2{margin-left:16.6666666667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.3333333333%}.col-xs-offset-5{margin-left:41.6666666667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.3333333333%}.col-xs-offset-8{margin-left:66.6666666667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.3333333333%}.col-xs-offset-11{margin-left:91.6666666667%}.col-xs-offset-12{margin-left:100%}@media(min-width: 768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-1{width:8.3333333333%}.col-sm-2{width:16.6666666667%}.col-sm-3{width:25%}.col-sm-4{width:33.3333333333%}.col-sm-5{width:41.6666666667%}.col-sm-6{width:50%}.col-sm-7{width:58.3333333333%}.col-sm-8{width:66.6666666667%}.col-sm-9{width:75%}.col-sm-10{width:83.3333333333%}.col-sm-11{width:91.6666666667%}.col-sm-12{width:100%}.col-sm-pull-0{right:auto}.col-sm-pull-1{right:8.3333333333%}.col-sm-pull-2{right:16.6666666667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.3333333333%}.col-sm-pull-5{right:41.6666666667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.3333333333%}.col-sm-pull-8{right:66.6666666667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.3333333333%}.col-sm-pull-11{right:91.6666666667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:auto}.col-sm-push-1{left:8.3333333333%}.col-sm-push-2{left:16.6666666667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.3333333333%}.col-sm-push-5{left:41.6666666667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.3333333333%}.col-sm-push-8{left:66.6666666667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.3333333333%}.col-sm-push-11{left:91.6666666667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0%}.col-sm-offset-1{margin-left:8.3333333333%}.col-sm-offset-2{margin-left:16.6666666667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.3333333333%}.col-sm-offset-5{margin-left:41.6666666667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.3333333333%}.col-sm-offset-8{margin-left:66.6666666667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.3333333333%}.col-sm-offset-11{margin-left:91.6666666667%}.col-sm-offset-12{margin-left:100%}}@media(min-width: 992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-1{width:8.3333333333%}.col-md-2{width:16.6666666667%}.col-md-3{width:25%}.col-md-4{width:33.3333333333%}.col-md-5{width:41.6666666667%}.col-md-6{width:50%}.col-md-7{width:58.3333333333%}.col-md-8{width:66.6666666667%}.col-md-9{width:75%}.col-md-10{width:83.3333333333%}.col-md-11{width:91.6666666667%}.col-md-12{width:100%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.3333333333%}.col-md-pull-2{right:16.6666666667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.3333333333%}.col-md-pull-5{right:41.6666666667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.3333333333%}.col-md-pull-8{right:66.6666666667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.3333333333%}.col-md-pull-11{right:91.6666666667%}.col-md-pull-12{right:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.3333333333%}.col-md-push-2{left:16.6666666667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.3333333333%}.col-md-push-5{left:41.6666666667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.3333333333%}.col-md-push-8{left:66.6666666667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.3333333333%}.col-md-push-11{left:91.6666666667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0%}.col-md-offset-1{margin-left:8.3333333333%}.col-md-offset-2{margin-left:16.6666666667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.3333333333%}.col-md-offset-5{margin-left:41.6666666667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.3333333333%}.col-md-offset-8{margin-left:66.6666666667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.3333333333%}.col-md-offset-11{margin-left:91.6666666667%}.col-md-offset-12{margin-left:100%}}@media(min-width: 1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-1{width:8.3333333333%}.col-lg-2{width:16.6666666667%}.col-lg-3{width:25%}.col-lg-4{width:33.3333333333%}.col-lg-5{width:41.6666666667%}.col-lg-6{width:50%}.col-lg-7{width:58.3333333333%}.col-lg-8{width:66.6666666667%}.col-lg-9{width:75%}.col-lg-10{width:83.3333333333%}.col-lg-11{width:91.6666666667%}.col-lg-12{width:100%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.3333333333%}.col-lg-pull-2{right:16.6666666667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.3333333333%}.col-lg-pull-5{right:41.6666666667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.3333333333%}.col-lg-pull-8{right:66.6666666667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.3333333333%}.col-lg-pull-11{right:91.6666666667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.3333333333%}.col-lg-push-2{left:16.6666666667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.3333333333%}.col-lg-push-5{left:41.6666666667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.3333333333%}.col-lg-push-8{left:66.6666666667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.3333333333%}.col-lg-push-11{left:91.6666666667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0%}.col-lg-offset-1{margin-left:8.3333333333%}.col-lg-offset-2{margin-left:16.6666666667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.3333333333%}.col-lg-offset-5{margin-left:41.6666666667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.3333333333%}.col-lg-offset-8{margin-left:66.6666666667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.3333333333%}.col-lg-offset-11{margin-left:91.6666666667%}.col-lg-offset-12{margin-left:100%}}table{background-color:rgba(0,0,0,0);color:#444}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:18px}.table>thead>tr>th,.table>thead>tr>td,.table>tbody>tr>th,.table>tbody>tr>td,.table>tfoot>tr>th,.table>tfoot>tr>td{padding:10px 0px 10px 20px;line-height:1.428571429;vertical-align:top;border-top:1px solid #eee}.table>thead>tr>th{vertical-align:bottom;border-bottom:1px solid #eee}.table>caption+thead>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>th,.table>thead:first-child>tr:first-child>td{border-top:0;font-family:"Inter";font-weight:normal}.table>tbody+tbody{border-top:2px solid #eee}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>tfoot>tr>td{padding:8px}.table-bordered{border:1px solid #eee}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>tfoot>tr>td{border:1px solid #eee}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#fbfbfb}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>thead>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>thead>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th{background-color:#7ebc59}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#70b348}.table>thead>tr>td.info,.table>thead>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>thead>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>thead>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th{background-color:#ee451f}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#e23811}@media screen and (max-width: 767px){.table-responsive{width:100%;margin-bottom:13.5px;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:18px;font-size:19.5px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:normal}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9 ;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:none}output{display:block;padding-top:7px;font-size:13px;line-height:1.428571429;color:#555}select,textarea,input[type=text],input[type=password],input[type=datetime],input[type=datetime-local],input[type=date],input[type=month],input[type=time],input[type=week],input[type=number],input[type=email],input[type=url],input[type=search],input[type=tel],input[type=color]{display:block;width:100%;height:32px;padding:6px 12px;font-size:13px;line-height:1.428571429;color:#555;background-color:#fff;background-image:none;border:1px solid #e4eaec;border-radius:3px;text-overflow:ellipsis;max-width:450px;transition:none;-webkit-transition:border-color ease-in-out .1s,box-shadow ease-in-out .1s;-o-transition:border-color ease-in-out .1s,box-shadow ease-in-out .1s;transition:border-color ease-in-out .1s,box-shadow ease-in-out .1s}select:focus,textarea:focus,input[type=text]:focus,input[type=password]:focus,input[type=datetime]:focus,input[type=datetime-local]:focus,input[type=date]:focus,input[type=month]:focus,input[type=time]:focus,input[type=week]:focus,input[type=number]:focus,input[type=email]:focus,input[type=url]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=color]:focus{border-color:#66afe9;outline:0;transition:border-color 250ms ease}select::-moz-placeholder,textarea::-moz-placeholder,input[type=text]::-moz-placeholder,input[type=password]::-moz-placeholder,input[type=datetime]::-moz-placeholder,input[type=datetime-local]::-moz-placeholder,input[type=date]::-moz-placeholder,input[type=month]::-moz-placeholder,input[type=time]::-moz-placeholder,input[type=week]::-moz-placeholder,input[type=number]::-moz-placeholder,input[type=email]::-moz-placeholder,input[type=url]::-moz-placeholder,input[type=search]::-moz-placeholder,input[type=tel]::-moz-placeholder,input[type=color]::-moz-placeholder{color:#777;opacity:1}select:-ms-input-placeholder,textarea:-ms-input-placeholder,input[type=text]:-ms-input-placeholder,input[type=password]:-ms-input-placeholder,input[type=datetime]:-ms-input-placeholder,input[type=datetime-local]:-ms-input-placeholder,input[type=date]:-ms-input-placeholder,input[type=month]:-ms-input-placeholder,input[type=time]:-ms-input-placeholder,input[type=week]:-ms-input-placeholder,input[type=number]:-ms-input-placeholder,input[type=email]:-ms-input-placeholder,input[type=url]:-ms-input-placeholder,input[type=search]:-ms-input-placeholder,input[type=tel]:-ms-input-placeholder,input[type=color]:-ms-input-placeholder{color:#777}select::-webkit-input-placeholder,textarea::-webkit-input-placeholder,input[type=text]::-webkit-input-placeholder,input[type=password]::-webkit-input-placeholder,input[type=datetime]::-webkit-input-placeholder,input[type=datetime-local]::-webkit-input-placeholder,input[type=date]::-webkit-input-placeholder,input[type=month]::-webkit-input-placeholder,input[type=time]::-webkit-input-placeholder,input[type=week]::-webkit-input-placeholder,input[type=number]::-webkit-input-placeholder,input[type=email]::-webkit-input-placeholder,input[type=url]::-webkit-input-placeholder,input[type=search]::-webkit-input-placeholder,input[type=tel]::-webkit-input-placeholder,input[type=color]::-webkit-input-placeholder{color:#777}select[disabled],select[readonly],fieldset[disabled] select,textarea[disabled],textarea[readonly],fieldset[disabled] textarea,input[type=text][disabled],input[type=text][readonly],fieldset[disabled] input[type=text],input[type=password][disabled],input[type=password][readonly],fieldset[disabled] input[type=password],input[type=datetime][disabled],input[type=datetime][readonly],fieldset[disabled] input[type=datetime],input[type=datetime-local][disabled],input[type=datetime-local][readonly],fieldset[disabled] input[type=datetime-local],input[type=date][disabled],input[type=date][readonly],fieldset[disabled] input[type=date],input[type=month][disabled],input[type=month][readonly],fieldset[disabled] input[type=month],input[type=time][disabled],input[type=time][readonly],fieldset[disabled] input[type=time],input[type=week][disabled],input[type=week][readonly],fieldset[disabled] input[type=week],input[type=number][disabled],input[type=number][readonly],fieldset[disabled] input[type=number],input[type=email][disabled],input[type=email][readonly],fieldset[disabled] input[type=email],input[type=url][disabled],input[type=url][readonly],fieldset[disabled] input[type=url],input[type=search][disabled],input[type=search][readonly],fieldset[disabled] input[type=search],input[type=tel][disabled],input[type=tel][readonly],fieldset[disabled] input[type=tel],input[type=color][disabled],input[type=color][readonly],fieldset[disabled] input[type=color]{cursor:not-allowed;background-color:#eee;opacity:1}textarea{height:auto}input[type=search]{-webkit-appearance:none}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:32px;line-height:1.428571429 \0 }input[type=date].input-sm,.input-group-sm>input[type=date].form-control,.input-group-sm>input[type=date].input-group-addon,.input-group-sm>.input-group-btn>input[type=date].btn,.form-horizontal .form-group-sm input[type=date].form-control,input[type=time].input-sm,.input-group-sm>input[type=time].form-control,.input-group-sm>input[type=time].input-group-addon,.input-group-sm>.input-group-btn>input[type=time].btn,.form-horizontal .form-group-sm input[type=time].form-control,input[type=datetime-local].input-sm,.input-group-sm>input[type=datetime-local].form-control,.input-group-sm>input[type=datetime-local].input-group-addon,.input-group-sm>.input-group-btn>input[type=datetime-local].btn,.form-horizontal .form-group-sm input[type=datetime-local].form-control,input[type=month].input-sm,.input-group-sm>input[type=month].form-control,.input-group-sm>input[type=month].input-group-addon,.input-group-sm>.input-group-btn>input[type=month].btn,.form-horizontal .form-group-sm input[type=month].form-control{line-height:30px}input[type=date].input-lg,.input-group-lg>input[type=date].form-control,.input-group-lg>input[type=date].input-group-addon,.input-group-lg>.input-group-btn>input[type=date].btn,.form-horizontal .form-group-lg input[type=date].form-control,input[type=time].input-lg,.input-group-lg>input[type=time].form-control,.input-group-lg>input[type=time].input-group-addon,.input-group-lg>.input-group-btn>input[type=time].btn,.form-horizontal .form-group-lg input[type=time].form-control,input[type=datetime-local].input-lg,.input-group-lg>input[type=datetime-local].form-control,.input-group-lg>input[type=datetime-local].input-group-addon,.input-group-lg>.input-group-btn>input[type=datetime-local].btn,.form-horizontal .form-group-lg input[type=datetime-local].form-control,input[type=month].input-lg,.input-group-lg>input[type=month].form-control,.input-group-lg>input[type=month].input-group-addon,.input-group-lg>.input-group-btn>input[type=month].btn,.form-horizontal .form-group-lg input[type=month].form-control{line-height:45px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:18px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{position:absolute;margin-left:-20px;margin-top:4px \9 }.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=radio].disabled,fieldset[disabled] input[type=radio],input[type=checkbox][disabled],input[type=checkbox].disabled,fieldset[disabled] input[type=checkbox]{cursor:not-allowed}.radio-inline.disabled,fieldset[disabled] .radio-inline,.checkbox-inline.disabled,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,fieldset[disabled] .radio label,.checkbox.disabled label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.form-horizontal .form-group-lg .form-control-static.form-control,.form-control-static.input-sm,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn,.form-horizontal .form-group-sm .form-control-static.form-control{padding-left:0;padding-right:0}.input-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn,.form-horizontal .form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,.input-group-sm>.input-group-btn>select.btn,.form-horizontal .form-group-sm select.form-control{height:30px;line-height:30px}textarea.input-sm,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,.input-group-sm>.input-group-btn>textarea.btn,.form-horizontal .form-group-sm textarea.form-control,select[multiple].input-sm,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>.input-group-btn>select[multiple].btn,.form-horizontal .form-group-sm select[multiple].form-control{height:auto}.input-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn,.form-horizontal .form-group-lg .form-control{height:45px;padding:10px 16px;font-size:17px;line-height:1.33;border-radius:6px}select.input-lg,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,.input-group-lg>.input-group-btn>select.btn,.form-horizontal .form-group-lg select.form-control{height:45px;line-height:45px}textarea.input-lg,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,.input-group-lg>.input-group-btn>textarea.btn,.form-horizontal .form-group-lg textarea.form-control,select[multiple].input-lg,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>.input-group-btn>select[multiple].btn,.form-horizontal .form-group-lg select[multiple].form-control{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:40px}.form-control-feedback{position:absolute;top:23px;right:0;z-index:2;display:block;width:32px;height:32px;line-height:32px;text-align:center}.input-lg+.form-control-feedback,.input-group-lg>.form-control+.form-control-feedback,.input-group-lg>.input-group-addon+.form-control-feedback,.input-group-lg>.input-group-btn>.btn+.form-control-feedback,.form-horizontal .form-group-lg .form-control+.form-control-feedback{width:45px;height:45px;line-height:45px}.input-sm+.form-control-feedback,.input-group-sm>.form-control+.form-control-feedback,.input-group-sm>.input-group-addon+.form-control-feedback,.input-group-sm>.input-group-btn>.btn+.form-control-feedback,.form-horizontal .form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#7ebc59}.has-success .form-control{border-color:#7ebc59;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#65a141;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #b6d9a2;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #b6d9a2}.has-success .input-group-addon{color:#7ebc59;border-color:#7ebc59;background-color:#7ebc59}.has-success .form-control-feedback{color:#7ebc59}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#ec971f;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f8d9ac;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f8d9ac}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#f0ad4e}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#ee451f}.has-error .form-control{border-color:#ee451f;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#cb320f;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f5947e;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f5947e}.has-error .input-group-addon{color:#ee451f;border-color:#ee451f;background-color:#ee451f}.has-error .form-control-feedback{color:#ee451f}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#7c7c7a}@media(min-width: 768px){.form-inline .form-group,.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control,.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group,.navbar-form .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.navbar-form .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.navbar-form .input-group .input-group-btn,.form-inline .input-group .form-control,.navbar-form .input-group .form-control{width:auto}.form-inline .input-group>.form-control,.navbar-form .input-group>.form-control{width:100%}.form-inline .control-label,.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.navbar-form .radio,.form-inline .checkbox,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.navbar-form .radio label,.form-inline .checkbox label,.navbar-form .checkbox label{padding-left:0}.form-inline .radio input[type=radio],.navbar-form .radio input[type=radio],.form-inline .checkbox input[type=checkbox],.navbar-form .checkbox input[type=checkbox]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback,.navbar-form .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:25px}.form-horizontal .form-group{margin-left:-20px;margin-right:-20px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{content:" ";display:table}.form-horizontal .form-group:after{clear:both}@media(min-width: 768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .has-feedback control-feedback{top:0;right:20px}@media(min-width: 768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media(min-width: 768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid rgba(0,0,0,0);white-space:nowrap;padding:6px 12px;font-size:13px;line-height:1.428571429;border-radius:3px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#fff;background-color:#536270;border-color:#536270}.btn:hover,.btn:focus,.btn:active,.btn.active,.open>.btn.dropdown-toggle{color:#fff;background-color:#3d4853;border-color:#39434d}.btn:active,.btn.active,.open>.btn.dropdown-toggle{background-image:none}.btn.disabled,.btn.disabled:hover,.btn.disabled:focus,.btn.disabled:active,.btn.disabled.active,.btn[disabled],.btn[disabled]:hover,.btn[disabled]:focus,.btn[disabled]:active,.btn[disabled].active,fieldset[disabled] .btn,fieldset[disabled] .btn:hover,fieldset[disabled] .btn:focus,fieldset[disabled] .btn:active,fieldset[disabled] .btn.active{background-color:#536270;border-color:#536270}.btn .badge{color:#536270;background-color:#fff}.btn:focus,.btn:active:focus,.btn.active:focus{outline:none}.btn:hover,.btn:focus{color:#fff;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#fff;background-color:#536270;border-color:#536270}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{color:#fff;background-color:#3d4853;border-color:#39434d}.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{background-image:none}.btn-default.disabled,.btn-default.disabled:hover,.btn-default.disabled:focus,.btn-default.disabled:active,.btn-default.disabled.active,.btn-default[disabled],.btn-default[disabled]:hover,.btn-default[disabled]:focus,.btn-default[disabled]:active,.btn-default[disabled].active,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default:hover,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default.active{background-color:#536270;border-color:#536270}.btn-default .badge{color:#536270;background-color:#fff}.btn-primary{color:#fff;background-color:#51aded;border-color:#51aded}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#2397e8;border-color:#1a93e7}.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{background-image:none}.btn-primary.disabled,.btn-primary.disabled:hover,.btn-primary.disabled:focus,.btn-primary.disabled:active,.btn-primary.disabled.active,.btn-primary[disabled],.btn-primary[disabled]:hover,.btn-primary[disabled]:focus,.btn-primary[disabled]:active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary:hover,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary.active{background-color:#51aded;border-color:#51aded}.btn-primary .badge{color:#51aded;background-color:#fff}.btn-success{color:#fff;background-color:#7ebc59;border-color:#70b348}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#65a141;border-color:#558837}.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{background-image:none}.btn-success.disabled,.btn-success.disabled:hover,.btn-success.disabled:focus,.btn-success.disabled:active,.btn-success.disabled.active,.btn-success[disabled],.btn-success[disabled]:hover,.btn-success[disabled]:focus,.btn-success[disabled]:active,.btn-success[disabled].active,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success:hover,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success.active{background-color:#7ebc59;border-color:#70b348}.btn-success .badge{color:#7ebc59;background-color:#fff}.btn-info{color:#fff;background-color:#368cbf;border-color:#307dab}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#2b6f97;border-color:#235a7b}.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{background-image:none}.btn-info.disabled,.btn-info.disabled:hover,.btn-info.disabled:focus,.btn-info.disabled:active,.btn-info.disabled.active,.btn-info[disabled],.btn-info[disabled]:hover,.btn-info[disabled]:focus,.btn-info[disabled]:active,.btn-info[disabled].active,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info:hover,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info.active{background-color:#368cbf;border-color:#307dab}.btn-info .badge{color:#368cbf;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{background-image:none}.btn-warning.disabled,.btn-warning.disabled:hover,.btn-warning.disabled:focus,.btn-warning.disabled:active,.btn-warning.disabled.active,.btn-warning[disabled],.btn-warning[disabled]:hover,.btn-warning[disabled]:focus,.btn-warning[disabled]:active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning:hover,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#ee451f;border-color:#e23811}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#cb320f;border-color:#a92a0d}.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled,.btn-danger.disabled:hover,.btn-danger.disabled:focus,.btn-danger.disabled:active,.btn-danger.disabled.active,.btn-danger[disabled],.btn-danger[disabled]:hover,.btn-danger[disabled]:focus,.btn-danger[disabled]:active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger:hover,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger.active{background-color:#ee451f;border-color:#e23811}.btn-danger .badge{color:#ee451f;background-color:#fff}.btn-link{color:#51aded;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:rgba(0,0,0,0);-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:rgba(0,0,0,0)}.btn-link:hover,.btn-link:focus{color:#178adb;text-decoration:underline;background-color:rgba(0,0,0,0)}.btn-link[disabled]:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:hover,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:17px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid rgba(0,0,0,0);border-left:4px solid rgba(0,0,0,0)}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:13px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:3px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:8px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#51aded}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:rgba(0,0,0,0);background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width: 768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar:before,.btn-toolbar:after{content:" ";display:table}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle,.btn-group-lg.btn-group>.btn+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret,.btn-group-lg>.btn .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret,.dropup .btn-group-lg>.btn .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{content:" ";display:table}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:3px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:3px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{position:absolute;z-index:-1;opacity:0;filter:alpha(opacity=0)}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:13px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #e4eaec;border-radius:3px}.input-group-addon.input-sm,.form-horizontal .form-group-sm .input-group-addon.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.form-horizontal .form-group-lg .input-group-addon.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:17px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav:before,.nav:after{content:" ";display:table}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;background-color:rgba(0,0,0,0);cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#51aded}.nav .nav-divider{height:1px;margin:8px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #e5e5e5}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid rgba(0,0,0,0);border-radius:3px 3px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #e5e5e5}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:rgba(0,0,0,0);cursor:default}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#51aded}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified,.nav-tabs.nav-justified{width:100%}.nav-justified>li,.nav-tabs.nav-justified>li{float:none}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{text-align:left;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width: 768px){.nav-justified>li,.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified,.nav-tabs.nav-justified{border-bottom:0}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:3px}.nav-tabs-justified>.active>a,.nav-tabs.nav-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width: 768px){.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:3px 3px 0 0}.nav-tabs-justified>.active>a,.nav-tabs.nav-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:0;border:1px solid rgba(0,0,0,0)}.navbar:before,.navbar:after{content:" ";display:table}.navbar:after{clear:both}@media(min-width: 768px){.navbar{border-radius:0}}.navbar-header:before,.navbar-header:after{content:" ";display:table}.navbar-header:after{clear:both}@media(min-width: 768px){.navbar-header{float:left}}.navbar-collapse{overflow-x:visible;padding-right:20px;padding-left:20px;border-top:1px solid rgba(0,0,0,0);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{content:" ";display:table}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width: 768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media(max-width: 480px)and (orientation: landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-header,.container-fluid>.navbar-collapse{margin-right:-20px;margin-left:-20px}@media(min-width: 768px){.container>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-header,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width: 768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}@media(min-width: 768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 20px;font-size:17px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width: 768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-20px}}.navbar-toggle{position:relative;float:left;margin-right:20px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:rgba(0,0,0,0);background-image:none;border:1px solid rgba(0,0,0,0);border-radius:3px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width: 768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -20px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:18px}@media(max-width: 767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:rgba(0,0,0,0);border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:18px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width: 768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-20px}}@media(min-width: 768px){.navbar-left{float:left !important}.navbar-right{float:right !important}}.navbar-form{margin-left:-20px;margin-right:-20px;padding:10px 0px;border-top:1px solid rgba(0,0,0,0);border-bottom:1px solid rgba(0,0,0,0);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:9px;margin-bottom:9px}@media(max-width: 767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width: 768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-20px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:9px;margin-bottom:9px}.navbar-btn.btn-sm,.btn-group-sm>.navbar-btn.btn{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs,.btn-group-xs>.navbar-btn.btn{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:16px;margin-bottom:16px}@media(min-width: 768px){.navbar-text{float:left;margin-left:20px;margin-right:20px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#3c3c3b;border-color:#2b2b2b}.navbar-default .navbar-brand{color:#f7f7f7}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#dedede;background-color:rgba(0,0,0,0)}.navbar-default .navbar-text{color:#f7f7f7}.navbar-default .navbar-nav>li>a{color:#f7f7f7}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{background-color:rgba(0,0,0,0)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#2b2b2b}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:rgba(0,0,0,0)}.navbar-default .navbar-toggle{border-color:#fff}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#555}.navbar-default .navbar-toggle .icon-bar{background-color:#fff}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#2b2b2b}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#2b2b2b;color:#555}@media(max-width: 767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#f7f7f7}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{background-color:rgba(0,0,0,0)}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#2b2b2b}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:rgba(0,0,0,0)}}.navbar-default .navbar-link{color:#f7f7f7}.navbar-default .btn-link{color:#f7f7f7}.navbar-default .btn-link[disabled]:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:hover,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#090909}.navbar-inverse .navbar-brand{color:#777}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:rgba(0,0,0,0)}.navbar-inverse .navbar-text{color:#777}.navbar-inverse .navbar-nav>li>a{color:#777}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:rgba(0,0,0,0)}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:rgba(0,0,0,0)}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#090909;color:#fff}@media(max-width: 767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:rgba(0,0,0,0)}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:rgba(0,0,0,0)}}.navbar-inverse .navbar-link{color:#777}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#777}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:hover,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:18px;list-style:none;background-color:#f5f5f5;border-radius:3px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/ ";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:18px 0;border-radius:3px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.428571429;text-decoration:none;color:#51aded;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pagination>li>a:hover,.pagination>li>a:focus,.pagination>li>span:hover,.pagination>li>span:focus{color:#178adb;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:hover,.pagination>.active>a:focus,.pagination>.active>span,.pagination>.active>span:hover,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#51aded;border-color:#51aded;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:17px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:18px 0;list-style:none;text-align:center}.pager:before,.pager:after{content:" ";display:table}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label:empty{display:none}.btn .label{position:relative;top:-1px}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#51aded}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#2397e8}.label-success{background-color:#7ebc59}.label-success[href]:hover,.label-success[href]:focus{background-color:#65a141}.label-info{background-color:#368cbf}.label-info[href]:hover,.label-info[href]:focus{background-color:#2b6f97}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#ee451f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#cb320f}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#51aded;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:20px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width: 768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:58.5px}}.thumbnail{display:block;padding:4px;margin-bottom:18px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:3px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;width:100% \9 ;max-width:100%;height:auto;margin-left:auto;margin-right:auto}.thumbnail .caption{padding:9px;color:#3c3c3b}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#51aded}.alert{padding:15px;margin-bottom:18px;border:1px solid rgba(0,0,0,0);border-radius:3px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#7ebc59;border-color:#7ebc59;color:#4e7d32}.alert-success hr{border-top-color:#70b348}.alert-success .alert-link{color:#598f3a}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#173543}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#1d4356}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#c77c11}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#df8a13}.alert-danger{background-color:#ee451f;border-color:#ee451f;color:#9b260c}.alert-danger hr{border-top-color:#e23811}.alert-danger .alert-link{color:#b32c0e}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:18px;background-color:#f5f5f5;border-radius:3px;position:relative;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:18px;color:#fff;text-align:center;background-color:#51aded;position:relative;z-index:2;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar[aria-valuenow="1"],.progress-bar[aria-valuenow="2"]{min-width:30px}.progress-bar[aria-valuenow="0"]{color:#777;min-width:30px;background-color:rgba(0,0,0,0);background-image:none;box-shadow:none}.progress-bar-success{background-color:#7ebc59}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#368cbf}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#ee451f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:6px 8px;margin-bottom:-1px;background-color:#fff}.list-group-item:last-child{margin-bottom:0}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#3c3c3b;border-radius:0}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item:hover:before,a.list-group-item:focus:before{background:#3498db;content:"";height:42px;left:0;position:absolute;top:0;width:3px}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eee;color:#777}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2}.list-group-item.active:before,.list-group-item.active:hover:before,.list-group-item.active:focus:before{background:#3498db;content:"";height:42px;left:0;position:absolute;top:0px;width:3px}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#fff}.list-group-item.active+.collapse>.list-group-item:before,.list-group-item.active:hover+.collapse>.list-group-item:before,.list-group-item.active:focus+.collapse>.list-group-item:before{background:#3498db;content:"";height:42px;left:0;position:absolute;top:0px;width:3px}.list-group-item-success{color:#7ebc59;background-color:#7ebc59}a.list-group-item-success{color:#7ebc59}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#7ebc59;background-color:#70b348}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#7ebc59;border-color:#7ebc59}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#f0ad4e;background-color:#fcf8e3}a.list-group-item-warning{color:#f0ad4e}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#f0ad4e;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.list-group-item-danger{color:#ee451f;background-color:#ee451f}a.list-group-item-danger{color:#ee451f}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#ee451f;background-color:#e23811}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#ee451f;border-color:#ee451f}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:18px;background-color:#fff;border:1px solid rgba(0,0,0,0);border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{content:" ";display:table}.panel-body:after{clear:both}.panel-heading{padding:10px 15px;border-bottom:1px solid rgba(0,0,0,0);border-top-right-radius:2px;border-top-left-radius:2px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:15px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:2px;border-top-left-radius:2px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:2px;border-top-left-radius:2px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:2px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:2px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:2px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:2px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #eee}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:18px}.panel-group .panel{margin-bottom:0;border-radius:3px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#51aded}.panel-primary>.panel-heading{color:#fff;background-color:#51aded;border-color:#51aded}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#51aded}.panel-primary>.panel-heading .badge{color:#51aded;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#51aded}.panel-success{border-color:#7ebc59}.panel-success>.panel-heading{color:#7ebc59;background-color:#7ebc59;border-color:#7ebc59}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#7ebc59}.panel-success>.panel-heading .badge{color:#7ebc59;background-color:#7ebc59}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#7ebc59}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#f0ad4e;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#f0ad4e}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ee451f}.panel-danger>.panel-heading{color:#ee451f;background-color:#ee451f;border-color:#ee451f}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ee451f}.panel-danger>.panel-heading .badge{color:#ee451f;background-color:#ee451f}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ee451f}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:3px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:19.5px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:rgba(0,0,0,0);border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate3d(0, -25%, 0);transform:translate3d(0, -25%, 0);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:62px 10px 10px 10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.428571429px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{content:" ";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media(min-width: 768px){.modal-dialog{width:600px;margin:82px auto 30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media(min-width: 992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:3px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:rgba(0,0,0,0);border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:13px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:rgba(0,0,0,0);border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;width:100% \9 ;max-width:100%;height:auto;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);background-image:-o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);background-image:linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000", endColorstr="#00000000", GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);background-image:-o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);background-image:linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#00000000", endColorstr="#80000000", GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif}.carousel-control .icon-prev:before{content:"‹"}.carousel-control .icon-next:before{content:"›"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9 ;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width: 768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.content-box:before,.clearfix:after,.content-box:after{content:" ";display:table}.clearfix:after,.content-box:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:rgba(0,0,0,0);text-shadow:none;background-color:rgba(0,0,0,0);border:0}.hidden{display:none !important;visibility:hidden !important}.affix{position:fixed;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media(max-width: 767px){.visible-xs{display:block !important}table.visible-xs{display:table}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media(max-width: 767px){.visible-xs-block{display:block !important}}@media(max-width: 767px){.visible-xs-inline{display:inline !important}}@media(max-width: 767px){.visible-xs-inline-block{display:inline-block !important}}@media(min-width: 768px)and (max-width: 991px){.visible-sm{display:block !important}table.visible-sm{display:table}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media(min-width: 768px)and (max-width: 991px){.visible-sm-block{display:block !important}}@media(min-width: 768px)and (max-width: 991px){.visible-sm-inline{display:inline !important}}@media(min-width: 768px)and (max-width: 991px){.visible-sm-inline-block{display:inline-block !important}}@media(min-width: 992px)and (max-width: 1199px){.visible-md{display:block !important}table.visible-md{display:table}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media(min-width: 992px)and (max-width: 1199px){.visible-md-block{display:block !important}}@media(min-width: 992px)and (max-width: 1199px){.visible-md-inline{display:inline !important}}@media(min-width: 992px)and (max-width: 1199px){.visible-md-inline-block{display:inline-block !important}}@media(min-width: 1200px){.visible-lg{display:block !important}table.visible-lg{display:table}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media(min-width: 1200px){.visible-lg-block{display:block !important}}@media(min-width: 1200px){.visible-lg-inline{display:inline !important}}@media(min-width: 1200px){.visible-lg-inline-block{display:inline-block !important}}@media(max-width: 767px){.hidden-xs,.page-side{display:none !important}}@media(min-width: 768px)and (max-width: 991px){.hidden-sm{display:none !important}}@media(max-width: 768px),(max-height: 669px){.toggle-sidebar{display:none !important}}@media(min-width: 992px)and (max-width: 1199px){.hidden-md{display:none !important}}@media(min-width: 1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}*{-webkit-font-smoothing:antialiased}html,body{font-family:"Inter",serif;font-weight:400;height:100%;background-color:#fff}body{min-width:320px;touch-action:manipulation}.widget-sort-handle{touch-action:none}.page-head{position:fixed;z-index:2;top:0;right:0;left:0;background:#3c3c3b}.page-head .navbar-default{min-height:60px;color:rgba(87,87,87,.67);border:0;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.08)}.page-head .navbar-text{color:#494949}.page-head .navbar-right{margin-top:6px}.page-head .navbar-brand{display:flex;flex-direction:column;width:250px;height:100%;text-align:center;color:#000;background:#3498db}.page-head .navbar-brand::before{font-size:16px;font-weight:500;display:inline-block;content:"OPNSense";color:#fff}.page-head .navbar-brand::after{font-size:12px;font-weight:400;display:inline-block;content:"AdvancedOPN Theme";color:#fafafa}.page-head .navbar-brand:hover,.page-head .navbar-brand:focus{background:#3498db}.page-head .navbar-brand .brand-logo,.page-head .navbar-brand .brand-icon{width:1px;height:1px}.page-head .navbar-toggle{margin-top:14px}.page-head .navbar-toggle .icon-bar{background-color:#363636}.page-head .navbar-toggle:hover,.page-head .navbar-toggle:focus{background-color:rgba(0,0,0,0)}.page-head .navbar-toggle:hover .icon-bar,.page-head .navbar-toggle:focus .icon-bar{background-color:#3498db}.page-content{position:relative;z-index:1;height:calc(100% - 60px);padding-top:60px}.page-content>.row{height:100%}@media(min-width: 768px){.page-content.col-lg-10,.page-content.col-sm-9{width:calc(100% - 250px)}.page-content.col-lg-push-2,.page-content.col-sm-push-3{left:250px}}.page-content-head,.content-box-head{padding-top:20px;padding-bottom:15px;border-bottom:1px solid rgba(217,217,217,.5);background:#fbfbfb}.page-content-head .navbar-nav,.content-box-head .navbar-nav{width:100%}.page-content-head h1,.content-box-head h1,.page-content-head h2,.content-box-head h2,.page-content-head h3,.content-box-head h3{line-height:1;margin:0}.page-content-main{min-height:calc(100% - 64px);padding:15px 0 73px;background:#f7f7f7}.page-side{position:fixed;z-index:3;top:0;left:0;overflow:auto;width:250px;height:100% !important;height:calc(100% - 60px) !important;margin-top:60px;border-right:1px solid rgba(0,0,0,.15);background:#263238}.page-side-nav--active{border-left:3px solid;background:#f7f7f7}.page-side-nav .panel{background:#263238}.page-foot{font-size:12px;position:fixed;z-index:2;bottom:0;width:100%;padding:20px 0;border-top:1px solid rgba(217,217,217,.5);background:#fbfbfb}.content-box{border:1px solid #eaeaea;border-radius:3px;background:#fff}.content-box-main{padding-top:15px;padding-bottom:15px}div.content-box.wizard div img{filter:invert(25%)}.tab-content{padding:0px 0;border-top:0px}.tab-content>.tab-content{margin-bottom:0;padding:0 15px}.tab-content .tab-content:last-child{margin-bottom:0}.page-content-main section[class^=col-]+section[class^=col-]{padding-top:20px}.brand-logo{display:none}@media(min-width: 768px){.brand-logo{display:inline-block}}.brand-icon{display:inline-block}@media(min-width: 768px){.brand-icon{display:none}}@media(min-width: 768px){.col-sm-disable-spacer{padding-top:0 !important}}@media(min-width: 992px){.col-md-disable-spacer{padding-top:0 !important}}@media(min-width: 1200px){.col-lg-disable-spacer{padding-top:0 !important}}.page-login{background:linear-gradient(#eff2f5, #f5f7f8)}.page-login .container{display:flex;align-items:center;flex-direction:column;justify-content:center;min-height:100%}.page-login .container:after{height:60px}.login-foot{font-size:12px}.login-modal-container{width:100%;max-width:400px;margin-bottom:8px;border-radius:4px;background:#fff;box-shadow:0 1px 8px rgba(0,0,0,.1)}.login-modal-head{height:50px;padding:0 20px;border-top-left-radius:4px;border-top-right-radius:4px;background:#51aded}.login-modal-head img{display:none}.login-modal-head .navbar-brand{display:flex;float:none;align-items:center;flex-direction:column;justify-content:center;text-align:center}.login-modal-head .navbar-brand::before{font-size:16px;font-weight:500;display:inline-block;content:"OPNSense";color:#fff}.login-modal-head .navbar-brand::after{font-size:11px;font-weight:400;display:inline-block;content:"AdvancedOPN Theme";color:#fafafa}.login-modal-content{padding:20px 20px 20px 20px}.login-modal-foot{height:60px;padding:20px 20px 0 20px;border-top:1px solid #eaeaea;background:#f7f7f7}.login-modal-foot a{text-decoration:none;color:#7d7d7d}.login-modal-foot a:hover{text-decoration:underline;color:#646464}@media(min-width: 768px){.list-inline .btn-group-container{float:right}}.btn.btn-fixed{width:100%;max-width:174px}.progress-bar-placeholder{font-size:12px;position:absolute;z-index:1;width:100%;text-align:center}.list-group-item{border-right:none;border-left:none}.list-group-item.collapsed .caret{border-top:0;border-bottom:4px solid green}main.page-content.col-lg-12{padding-left:90px}#navigation .panel.list-group .fa:not(.__iconspacer){width:auto;padding-right:10px}#navigation .panel.list-group a.list-group-item{font-size:14px;position:relative;padding:12px 16px;text-decoration:none;color:#a6adb3;background:#263238}#navigation .panel.list-group a.list-group-item:hover{color:#e1e4e6;background:rgba(255,255,255,.03)}#navigation .panel.list-group a.list-group-item:hover::before{background-color:#3498db}#navigation .panel.list-group a.list-group-item.active{color:#e1e4e6;background:rgba(255,255,255,.02)}#navigation .panel.list-group>.list-group-item:not(.collapsed).active-menu-title::before{width:3px;transition:opacity 200ms;background-color:#3498db}#navigation .panel.list-group>div .list-group-item{font-size:13px;position:relative;padding:5px 0 5px 35px}#navigation .panel.list-group>div .list-group-item::before{position:absolute;z-index:1;top:-11px;bottom:13px;left:12px;display:block;width:0;content:"";opacity:.8;border-left:1px dotted #3e474e !important}#navigation .panel.list-group>div .list-group-item::after{position:absolute;z-index:1;top:13px;left:12px;display:block;width:12px;content:"";border-bottom:1px dotted #3e474e !important}#navigation .panel.list-group>div .list-group-item:hover::before,#navigation .panel.list-group>div .list-group-item.active::before,#navigation .panel.list-group>div .list-group-item:focus::before{opacity:1;background-color:#3e474e}#navigation.col-sidebar-left{overflow:hidden;width:70px;border:none !important;background-color:rgba(0,0,0,0) !important}#navigation.col-sidebar-left>div.row{height:100% !important}#navigation.col-sidebar-left>div.row>nav.page-side-nav{width:70px;height:100% !important;border-right:1px solid #304047;background:#263238}#navigation.col-sidebar-left>div>nav>#mainmenu>div>a.list-group-item{font-size:13px;width:70px;height:70px;padding-top:12px;padding-right:0px;padding-left:0px;text-align:center;color:#a6adb3;border-right:1px solid #304047;border-bottom:2px solid #lightergrey}#navigation.col-sidebar-left>div>nav>#mainmenu>div>a.list-group-item>span.fa,#navigation.col-sidebar-left>div>nav>#mainmenu>div>a.list-group-item>span.glyphicon{font-size:20px;visibility:visible}#navigation.col-sidebar-left>div>nav>#mainmenu>div>a.list-group-item>span.__iconspacer{display:block;height:30px;padding:0px;text-align:center}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapsing>a.list-group-item{font-size:14px !important;position:absolute !important;left:70px !important;display:block !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapsing>a.list-group-item{font-size:14px !important;position:absolute !important;left:166px !important;display:block !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>a.list-group-item,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapse.in>a.list-group-item{background-color:#263238 !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapse.in>a.list-group-item{font-size:14px !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>div.collapse>a.list-group-item,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>a.list-group-item{font-size:14px !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>div.collapse>a.list-group-item::before,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>div.collapse>a.list-group-item::after,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>a.list-group-item::before,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>a.list-group-item::after{display:none}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapsed>a.list-group-item{font-size:14px !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>a.list-group-item,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>div.collapse>a.list-group-item{padding:3px 8px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in{font-size:14px;position:absolute;z-index:10;left:70px;width:168px;margin-top:-70px;border:1px solid #304047;-moz-box-shadow:2px 2px 1px 0px rgba(234,234,234,.5);-webkit-box-shadow:2px 2px 1px 0px rgba(234,234,234,.5);box-shadow:2px 2px 1px 0px rgba(234,234,234,.5)}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapse.in{font-size:14px;position:absolute;z-index:10;left:166px;width:168px;margin-top:-26px;border:1px solid #304047;-moz-box-shadow:2px 2px 1px 0px rgba(234,234,234,.5);-webkit-box-shadow:2px 2px 1px 0px rgba(234,234,234,.5);box-shadow:2px 2px 1px 0px rgba(234,234,234,.5)}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapsing,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapsing{display:none}button.toggle-sidebar{font-size:14px;float:left;margin-top:20px;color:#575757;border:none;outline:none;background-color:rgba(0,0,0,0)}#navigation.collapse.in{display:block !important}.list-group-submenu .list-group-item:last-child,.collapse .list-group-item:last-child{border-bottom:none}ul.nav>li.dropdown>ul.dropdown-menu>li>a{padding:3px 10px}.nav-tabs{margin-right:1px;border-bottom:2px solid #eaeaea}.nav-tabs>li{margin-right:2px;border-radius:0px;border-top-right-radius:10px}.nav-tabs>li>a{margin-right:0px;cursor:pointer;color:#777;border-top-left-radius:4px;border-top-right-radius:4px;background:#fcfcfc}.nav-tabs>li>a{border:none;background:rgba(0,0,0,0)}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-bottom:2px solid #e0e0e0;background-color:#efefef}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#51aded;border-top:none;border-right:none;border-bottom:2px solid #51aded;border-left:none;background:rgba(0,0,0,0)}.nav-tabs>li>a.visible-lg-inline-block:not(.pull-right){padding-right:6px !important;padding-left:12px !important;border-top-right-radius:0px !important}.nav-tabs>li>a.visible-lg-inline-block.pull-right{padding-right:12px !important;padding-left:6px !important;border-left:0px !important;border-top-left-radius:0}.nav-tabs.nav-justified{border-right:1px solid #eaeaea}.nav-tabs.nav-justified>li{border-top:1px solid #eaeaea;border-bottom:1px solid #eaeaea;border-left:1px solid #eaeaea;border-radius:0px;background:#f7f7f7}.nav-tabs.nav-justified>li>a{font-family:"Inter",serif;color:#3c3c3b}@media(min-width: 768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid rgba(0,0,0,0)}}@media(max-width: 767px){.nav-tabs.nav-justified>li.active>a{border-right:0 !important}}@media(min-width: 768px){>li.active+li>a{border-left:1px solid rgba(0,0,0,0)}}>li:last-child>a{border-right:1px solid rgba(0,0,0,0) !important}@media(max-width: 767px){>li:last-child>a{margin-bottom:0}}#opn-status-list .btn{color:#565656;border:1px solid #ecedf1;background:rgba(0,0,0,0)}#opn-status-list .btn:hover{background:#f9f9f9}.bootstrap-dialog-title{font-size:15px;font-weight:500}.bootstrap-select .btn-default{color:#76838f;border:1px solid #e4eaec;background:#fff}.bootstrap-select .btn-default:focus{border-color:#66afe9;background:#fff}.bootstrap-select.open>.btn-default{color:#76838f;border-color:#66afe9;background:#fff}.bootstrap-select .dropdown-toggle:focus{outline-color:#fff !important}.btn .glyphicon{vertical-align:-1px}.btn-default .glyphicon{color:#757575}table{width:100%}.table{margin-bottom:0px !important}.nav-tabs-justified .nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified .nav-tabs.nav-justified>.active>a:focus{border:0px !important}.table th,strong,b{font-family:"Inter",serif;font-weight:500}.table>tbody>tr>td:last-child{padding-right:15px}.__nowrap{white-space:nowrap}.__nomb{margin-bottom:0}.__mb{margin-bottom:15px}.__mt{margin-top:15px}.__ml{margin-left:15px}.__mr{margin-right:15px}.__iconspacer{padding-right:10px}#mainmenu .glyphicon{vertical-align:-2px}.list-group-item{overflow:hidden;text-overflow:ellipsis}.list-group-item+div.collapse{margin-bottom:-1px}.list-group-item+div>a{padding-left:44px}.list-group-item:before{position:absolute;top:0px;left:0;width:0;height:42px;min-height:100%;content:"";transition:opacity 0ms;opacity:0}.list-group-submenu a{padding-left:56px}.active-menu-title,.active-menu a{position:relative;text-decoration:none;background-color:#242f35}.active-menu-title:before,.active-menu a:before{width:3px}.active-menu-title.active,.active-menu a.active{background-color:#242f35}.active-menu a:before{background:#242f35}.alert.alert-danger{color:#fff !important}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{border-radius:0 !important}.navbar-brand{padding:10px 20px}.label-opnsense{font-size:12px;line-height:1.5;display:inline-block;vertical-align:middle;border:1px solid rgba(0,0,0,0);border-radius:3px}.label-opnsense-sm{padding:5px 10px}.label-opnsense-xs,.btn-xs,.btn-group-xs>.btn{padding:1px 3px}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-button{width:8px;height:5px}::-webkit-scrollbar-track{border-radius:0;background:#dcdcdc;box-shadow:0px 0px 0px}::-webkit-scrollbar-thumb{border:thin solid #e5e5e5;border-radius:0px;background:#afafaf}::-webkit-scrollbar-thumb:hover{background:#e5e5e5}.widgetdiv{padding-top:0px !important;padding-bottom:20px}select{overflow:hidden;cursor:pointer;border:1px solid #ccc;background-image:url("/ui/themes/advanced/build/images/caret.png") !important;background-repeat:no-repeat;background-position:right;-webkit-appearance:none;-moz-appearance:none;appearance:none}#grid-log th[data-column-id=__timestamp__],#filter-log-entries th[data-column-id=__timestamp__]{min-width:3.5em}@media screen and (max-width: 767px){.table-responsive{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>tfoot>tr>td{white-space:normal}}label>input[type=checkbox],label>input[type=radio]{float:left;margin-right:.4em}input[type=checkbox],input[type=radio]{font:inherit;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin:0;cursor:pointer;transition:border-color 150ms ease,background-color 150ms ease;transform:translateY(1px);color:#3f3f3f;border:1px solid #e4eaec;border-radius:.15em;background-color:#fff;-webkit-appearance:none;appearance:none}input[type=checkbox]::before,input[type=radio]::before{visibility:hidden;width:10px;height:10px;content:"";transition:transform 120ms ease-out 60ms,opacity 120ms ease-out,visibility 120ms ease-out;transform:scale(0.65);opacity:0;background-color:#fafafa;box-shadow:inset 1em 1em var(--form-control-color);clip-path:polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%)}input[type=checkbox]:checked,input[type=radio]:checked{border-color:#3498db;background-color:#3498db}input[type=checkbox]:checked::before,input[type=radio]:checked::before{visibility:visible;transform:scale(1);opacity:1;background-color:#fff}input[type=checkbox]:hover,input[type=radio]:hover{border-color:#3498db}input[type=checkbox]:disabled,input[type=radio]:disabled{cursor:not-allowed;color:var(--form-control-disabled);--form-control-color: var(--form-control-disabled)}input[type=radio]{border-radius:50%}div[data-for*=help_for]{font-size:.85em;font-style:italic;padding-top:.5em}#log-settings label[for^=act]{margin-right:1.5em}#log-settings table>tbody>tr>td{vertical-align:middle}#log-settings select#filterlogentries,#log-settings select#filterlogentriesupdateinterval{width:5em}#log-settings select#filterlogentriesinterfaces{min-width:100%;max-width:100%}[data-state=lightcoral]{background-color:#f08080}[data-state=white]{background-color:#fff}[data-state=red]{background-color:red}.tokens-container{margin-top:0px;margin-bottom:0px}.actionBar .btn.btn-default{color:#767676;border-color:#d8d8d8;background:#fafafa}.bootgrid-table td.select-cell{overflow:visible;width:30px !important}.act_toggle{font-size:15px}.fa.command-toggle{font-size:16px;padding-top:5px;vertical-align:middle}.form-control:hover{border-color:#d1e5f2}.table input[type=checkbox]{margin-top:-4px;margin-bottom:-4px}.table .btn-xs,.table .btn-group-xs>.btn{margin-top:-4px;margin-bottom:-4px}/*# sourceMappingURL=main.css.map */ +.widgetdiv .content-box-head{overflow:hidden;min-height:25px !important;padding:8px !important;color:#3c3c3b !important;border-top-left-radius:3px;border-top-right-radius:3px;background:#e8e8e8 !important}.widgetdiv .content-box-head .btn-group .btn{color:#3c3c3b !important;border:0px;background:#e8e8e8 !important}.widgetdiv .content-box-head .btn-group .btn .glyphicon{color:#3c3c3b !important}.widgetdiv .content-box-head .list-inline li>h3{padding-top:4px}@font-face{font-family:"Inter";font-style:normal;font-weight:600;font-display:swap;src:url("/ui/themes/advanced/build/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Inter";font-style:normal;font-weight:500;font-display:swap;src:url("/ui/themes/advanced/build/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Inter";font-style:normal;font-weight:400;font-display:swap;src:url("/ui/themes/advanced/build/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}/*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:rgba(0,0,0,0)}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none !important;color:#000 !important;background:rgba(0,0,0,0) !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff !important}.navbar{display:none}.table td,.table th{background-color:#fff !important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}@font-face{font-family:"Glyphicons Halflings";src:url("../fonts/bootstrap/glyphicons-halflings-regular.eot");src:url("../fonts/bootstrap/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/bootstrap/glyphicons-halflings-regular.woff") format("woff"),url("../fonts/bootstrap/glyphicons-halflings-regular.ttf") format("truetype"),url("../fonts/bootstrap/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"*"}.glyphicon-plus:before{content:"+"}.glyphicon-euro:before{content:"€"}.glyphicon-minus:before{content:"−"}.glyphicon-cloud:before{content:"☁"}.glyphicon-envelope:before{content:"✉"}.glyphicon-pencil:before{content:"✏"}.glyphicon-glass:before{content:""}.glyphicon-music:before{content:""}.glyphicon-search:before{content:""}.glyphicon-heart:before{content:""}.glyphicon-star:before{content:""}.glyphicon-star-empty:before{content:""}.glyphicon-user:before{content:""}.glyphicon-film:before{content:""}.glyphicon-th-large:before{content:""}.glyphicon-th:before{content:""}.glyphicon-th-list:before{content:""}.glyphicon-ok:before{content:""}.glyphicon-remove:before{content:""}.glyphicon-zoom-in:before{content:""}.glyphicon-zoom-out:before{content:""}.glyphicon-off:before{content:""}.glyphicon-signal:before{content:""}.glyphicon-cog:before{content:""}.glyphicon-trash:before{content:""}.glyphicon-home:before{content:""}.glyphicon-file:before{content:""}.glyphicon-time:before{content:""}.glyphicon-road:before{content:""}.glyphicon-download-alt:before{content:""}.glyphicon-download:before{content:""}.glyphicon-upload:before{content:""}.glyphicon-inbox:before{content:""}.glyphicon-play-circle:before{content:""}.glyphicon-repeat:before{content:""}.glyphicon-refresh:before{content:""}.glyphicon-list-alt:before{content:""}.glyphicon-lock:before{content:""}.glyphicon-flag:before{content:""}.glyphicon-headphones:before{content:""}.glyphicon-volume-off:before{content:""}.glyphicon-volume-down:before{content:""}.glyphicon-volume-up:before{content:""}.glyphicon-qrcode:before{content:""}.glyphicon-barcode:before{content:""}.glyphicon-tag:before{content:""}.glyphicon-tags:before{content:""}.glyphicon-book:before{content:""}.glyphicon-bookmark:before{content:""}.glyphicon-print:before{content:""}.glyphicon-camera:before{content:""}.glyphicon-font:before{content:""}.glyphicon-bold:before{content:""}.glyphicon-italic:before{content:""}.glyphicon-text-height:before{content:""}.glyphicon-text-width:before{content:""}.glyphicon-align-left:before{content:""}.glyphicon-align-center:before{content:""}.glyphicon-align-right:before{content:""}.glyphicon-align-justify:before{content:""}.glyphicon-list:before{content:""}.glyphicon-indent-left:before{content:""}.glyphicon-indent-right:before{content:""}.glyphicon-facetime-video:before{content:""}.glyphicon-picture:before{content:""}.glyphicon-map-marker:before{content:""}.glyphicon-adjust:before{content:""}.glyphicon-tint:before{content:""}.glyphicon-edit:before{content:""}.glyphicon-share:before{content:""}.glyphicon-check:before{content:""}.glyphicon-move:before{content:""}.glyphicon-step-backward:before{content:""}.glyphicon-fast-backward:before{content:""}.glyphicon-backward:before{content:""}.glyphicon-play:before{content:""}.glyphicon-pause:before{content:""}.glyphicon-stop:before{content:""}.glyphicon-forward:before{content:""}.glyphicon-fast-forward:before{content:""}.glyphicon-step-forward:before{content:""}.glyphicon-eject:before{content:""}.glyphicon-chevron-left:before{content:""}.glyphicon-chevron-right:before{content:""}.glyphicon-plus-sign:before{content:""}.glyphicon-minus-sign:before{content:""}.glyphicon-remove-sign:before{content:""}.glyphicon-ok-sign:before{content:""}.glyphicon-question-sign:before{content:""}.glyphicon-info-sign:before{content:""}.glyphicon-screenshot:before{content:""}.glyphicon-remove-circle:before{content:""}.glyphicon-ok-circle:before{content:""}.glyphicon-ban-circle:before{content:""}.glyphicon-arrow-left:before{content:""}.glyphicon-arrow-right:before{content:""}.glyphicon-arrow-up:before{content:""}.glyphicon-arrow-down:before{content:""}.glyphicon-share-alt:before{content:""}.glyphicon-resize-full:before{content:""}.glyphicon-resize-small:before{content:""}.glyphicon-exclamation-sign:before{content:""}.glyphicon-gift:before{content:""}.glyphicon-leaf:before{content:""}.glyphicon-fire:before{content:""}.glyphicon-eye-open:before{content:""}.glyphicon-eye-close:before{content:""}.glyphicon-warning-sign:before{content:""}.glyphicon-plane:before{content:""}.glyphicon-calendar:before{content:""}.glyphicon-random:before{content:""}.glyphicon-comment:before{content:""}.glyphicon-magnet:before{content:""}.glyphicon-chevron-up:before{content:""}.glyphicon-chevron-down:before{content:""}.glyphicon-retweet:before{content:""}.glyphicon-shopping-cart:before{content:""}.glyphicon-folder-close:before{content:""}.glyphicon-folder-open:before{content:""}.glyphicon-resize-vertical:before{content:""}.glyphicon-resize-horizontal:before{content:""}.glyphicon-hdd:before{content:""}.glyphicon-bullhorn:before{content:""}.glyphicon-bell:before{content:""}.glyphicon-certificate:before{content:""}.glyphicon-thumbs-up:before{content:""}.glyphicon-thumbs-down:before{content:""}.glyphicon-hand-right:before{content:""}.glyphicon-hand-left:before{content:""}.glyphicon-hand-up:before{content:""}.glyphicon-hand-down:before{content:""}.glyphicon-circle-arrow-right:before{content:""}.glyphicon-circle-arrow-left:before{content:""}.glyphicon-circle-arrow-up:before{content:""}.glyphicon-circle-arrow-down:before{content:""}.glyphicon-globe:before{content:""}.glyphicon-wrench:before{content:""}.glyphicon-tasks:before{content:""}.glyphicon-filter:before{content:""}.glyphicon-briefcase:before{content:""}.glyphicon-fullscreen:before{content:""}.glyphicon-dashboard:before{content:""}.glyphicon-paperclip:before{content:""}.glyphicon-heart-empty:before{content:""}.glyphicon-link:before{content:""}.glyphicon-phone:before{content:""}.glyphicon-pushpin:before{content:""}.glyphicon-usd:before{content:""}.glyphicon-gbp:before{content:""}.glyphicon-sort:before{content:""}.glyphicon-sort-by-alphabet:before{content:""}.glyphicon-sort-by-alphabet-alt:before{content:""}.glyphicon-sort-by-order:before{content:""}.glyphicon-sort-by-order-alt:before{content:""}.glyphicon-sort-by-attributes:before{content:""}.glyphicon-sort-by-attributes-alt:before{content:""}.glyphicon-unchecked:before{content:""}.glyphicon-expand:before{content:""}.glyphicon-collapse-down:before{content:""}.glyphicon-collapse-up:before{content:""}.glyphicon-log-in:before{content:""}.glyphicon-flash:before{content:""}.glyphicon-log-out:before{content:""}.glyphicon-new-window:before{content:""}.glyphicon-record:before{content:""}.glyphicon-save:before{content:""}.glyphicon-open:before{content:""}.glyphicon-saved:before{content:""}.glyphicon-import:before{content:""}.glyphicon-export:before{content:""}.glyphicon-send:before{content:""}.glyphicon-floppy-disk:before{content:""}.glyphicon-floppy-saved:before{content:""}.glyphicon-floppy-remove:before{content:""}.glyphicon-floppy-save:before{content:""}.glyphicon-floppy-open:before{content:""}.glyphicon-credit-card:before{content:""}.glyphicon-transfer:before{content:""}.glyphicon-cutlery:before{content:""}.glyphicon-header:before{content:""}.glyphicon-compressed:before{content:""}.glyphicon-earphone:before{content:""}.glyphicon-phone-alt:before{content:""}.glyphicon-tower:before{content:""}.glyphicon-stats:before{content:""}.glyphicon-sd-video:before{content:""}.glyphicon-hd-video:before{content:""}.glyphicon-subtitles:before{content:""}.glyphicon-sound-stereo:before{content:""}.glyphicon-sound-dolby:before{content:""}.glyphicon-sound-5-1:before{content:""}.glyphicon-sound-6-1:before{content:""}.glyphicon-sound-7-1:before{content:""}.glyphicon-copyright-mark:before{content:""}.glyphicon-registration-mark:before{content:""}.glyphicon-cloud-download:before{content:""}.glyphicon-cloud-upload:before{content:""}.glyphicon-tree-conifer:before{content:""}.glyphicon-tree-deciduous:before{content:""}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:1.428571429;color:#3c3c3b;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#51aded;text-decoration:none}a:hover,a:focus{color:#178adb;text-decoration:underline}a:focus{outline:none}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;width:100% \9 ;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:3px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;width:100% \9 ;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:18px;margin-bottom:18px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Inter";font-weight:500;line-height:1.4;color:inherit}h1 small,h1 .small,h2 small,h2 .small,h3 small,h3 .small,h4 small,h4 .small,h5 small,h5 .small,h6 small,h6 .small,.h1 small,.h1 .small,.h2 small,.h2 .small,.h3 small,.h3 .small,.h4 small,.h4 .small,.h5 small,.h5 .small,.h6 small,.h6 .small{font-weight:normal;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:18px;margin-bottom:9px}h1 small,h1 .small,.h1 small,.h1 .small,h2 small,h2 .small,.h2 small,.h2 .small,h3 small,h3 .small,.h3 small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:9px;margin-bottom:9px}h4 small,h4 .small,.h4 small,.h4 .small,h5 small,h5 .small,.h5 small,.h5 .small,h6 small,h6 .small,.h6 small,.h6 .small{font-size:75%}h1,.h1{font-size:22px}h2,.h2{font-size:14px}h3,.h3{font-size:13px}h4,.h4{font-size:17px}h5,.h5{font-size:13px}h6,.h6{font-size:12px}p{margin:0 0 9px}.lead{margin-bottom:18px;font-size:14px;font-weight:300;line-height:1.4}@media(min-width: 768px){.lead{font-size:19.5px}}small,.small{font-size:92%}cite{font-style:normal}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#51aded}a.text-primary:hover{color:#2397e8}.text-success{color:#7ebc59}a.text-success:hover{color:#65a141}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#f0ad4e}a.text-warning:hover{color:#ec971f}.text-danger{color:#ee451f}a.text-danger:hover{color:#cb320f}.bg-primary{color:#fff}.bg-primary{background-color:#51aded}a.bg-primary:hover{background-color:#2397e8}.bg-success{background-color:#7ebc59}a.bg-success:hover{background-color:#65a141}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#ee451f}a.bg-danger:hover{background-color:#cb320f}.page-header{padding-bottom:8px;margin:36px 0 18px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:9px}ul ul,ul ol,ol ul,ol ol{margin-bottom:0}.list-unstyled,.list-inline{padding-left:0;list-style:none}.list-inline{margin-left:-5px}.list-inline>li{float:left;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:18px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}.dl-horizontal dd:before,.dl-horizontal dd:after{content:" ";display:table}.dl-horizontal dd:after{clear:both}@media(min-width: 768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:9px 18px;margin:0 0 18px;font-size:16.25px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.428571429;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:"— "}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,.blockquote-reverse small:before,.blockquote-reverse .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before,blockquote.pull-right .small:before{content:""}.blockquote-reverse footer:after,.blockquote-reverse small:after,.blockquote-reverse .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after,blockquote.pull-right .small:after{content:" —"}blockquote:before,blockquote:after{content:""}address{margin-bottom:18px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:3px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{display:block;padding:calc((18px - 100%)/2);margin:0 0 9px;font-size:12px;line-height:1.428571429;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:3px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:rgba(0,0,0,0);border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:20px;padding-right:20px}.container:before,.container:after{content:" ";display:table}.container:after{clear:both}@media(min-width: 768px){.container{width:760px}}@media(min-width: 992px){.container{width:980px}}@media(min-width: 1200px){.container{width:1180px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:20px;padding-right:20px}.container-fluid:before,.container-fluid:after{content:" ";display:table}.container-fluid:after{clear:both}.row{margin-left:-20px;margin-right:-20px}.row:before,.row:after{content:" ";display:table}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:20px;padding-right:20px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-1{width:8.3333333333%}.col-xs-2{width:16.6666666667%}.col-xs-3{width:25%}.col-xs-4{width:33.3333333333%}.col-xs-5{width:41.6666666667%}.col-xs-6{width:50%}.col-xs-7{width:58.3333333333%}.col-xs-8{width:66.6666666667%}.col-xs-9{width:75%}.col-xs-10{width:83.3333333333%}.col-xs-11{width:91.6666666667%}.col-xs-12{width:100%}.col-xs-pull-0{right:auto}.col-xs-pull-1{right:8.3333333333%}.col-xs-pull-2{right:16.6666666667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.3333333333%}.col-xs-pull-5{right:41.6666666667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.3333333333%}.col-xs-pull-8{right:66.6666666667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.3333333333%}.col-xs-pull-11{right:91.6666666667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:auto}.col-xs-push-1{left:8.3333333333%}.col-xs-push-2{left:16.6666666667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.3333333333%}.col-xs-push-5{left:41.6666666667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.3333333333%}.col-xs-push-8{left:66.6666666667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.3333333333%}.col-xs-push-11{left:91.6666666667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0%}.col-xs-offset-1{margin-left:8.3333333333%}.col-xs-offset-2{margin-left:16.6666666667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.3333333333%}.col-xs-offset-5{margin-left:41.6666666667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.3333333333%}.col-xs-offset-8{margin-left:66.6666666667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.3333333333%}.col-xs-offset-11{margin-left:91.6666666667%}.col-xs-offset-12{margin-left:100%}@media(min-width: 768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-1{width:8.3333333333%}.col-sm-2{width:16.6666666667%}.col-sm-3{width:25%}.col-sm-4{width:33.3333333333%}.col-sm-5{width:41.6666666667%}.col-sm-6{width:50%}.col-sm-7{width:58.3333333333%}.col-sm-8{width:66.6666666667%}.col-sm-9{width:75%}.col-sm-10{width:83.3333333333%}.col-sm-11{width:91.6666666667%}.col-sm-12{width:100%}.col-sm-pull-0{right:auto}.col-sm-pull-1{right:8.3333333333%}.col-sm-pull-2{right:16.6666666667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.3333333333%}.col-sm-pull-5{right:41.6666666667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.3333333333%}.col-sm-pull-8{right:66.6666666667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.3333333333%}.col-sm-pull-11{right:91.6666666667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:auto}.col-sm-push-1{left:8.3333333333%}.col-sm-push-2{left:16.6666666667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.3333333333%}.col-sm-push-5{left:41.6666666667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.3333333333%}.col-sm-push-8{left:66.6666666667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.3333333333%}.col-sm-push-11{left:91.6666666667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0%}.col-sm-offset-1{margin-left:8.3333333333%}.col-sm-offset-2{margin-left:16.6666666667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.3333333333%}.col-sm-offset-5{margin-left:41.6666666667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.3333333333%}.col-sm-offset-8{margin-left:66.6666666667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.3333333333%}.col-sm-offset-11{margin-left:91.6666666667%}.col-sm-offset-12{margin-left:100%}}@media(min-width: 992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-1{width:8.3333333333%}.col-md-2{width:16.6666666667%}.col-md-3{width:25%}.col-md-4{width:33.3333333333%}.col-md-5{width:41.6666666667%}.col-md-6{width:50%}.col-md-7{width:58.3333333333%}.col-md-8{width:66.6666666667%}.col-md-9{width:75%}.col-md-10{width:83.3333333333%}.col-md-11{width:91.6666666667%}.col-md-12{width:100%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.3333333333%}.col-md-pull-2{right:16.6666666667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.3333333333%}.col-md-pull-5{right:41.6666666667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.3333333333%}.col-md-pull-8{right:66.6666666667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.3333333333%}.col-md-pull-11{right:91.6666666667%}.col-md-pull-12{right:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.3333333333%}.col-md-push-2{left:16.6666666667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.3333333333%}.col-md-push-5{left:41.6666666667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.3333333333%}.col-md-push-8{left:66.6666666667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.3333333333%}.col-md-push-11{left:91.6666666667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0%}.col-md-offset-1{margin-left:8.3333333333%}.col-md-offset-2{margin-left:16.6666666667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.3333333333%}.col-md-offset-5{margin-left:41.6666666667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.3333333333%}.col-md-offset-8{margin-left:66.6666666667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.3333333333%}.col-md-offset-11{margin-left:91.6666666667%}.col-md-offset-12{margin-left:100%}}@media(min-width: 1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-1{width:8.3333333333%}.col-lg-2{width:16.6666666667%}.col-lg-3{width:25%}.col-lg-4{width:33.3333333333%}.col-lg-5{width:41.6666666667%}.col-lg-6{width:50%}.col-lg-7{width:58.3333333333%}.col-lg-8{width:66.6666666667%}.col-lg-9{width:75%}.col-lg-10{width:83.3333333333%}.col-lg-11{width:91.6666666667%}.col-lg-12{width:100%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.3333333333%}.col-lg-pull-2{right:16.6666666667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.3333333333%}.col-lg-pull-5{right:41.6666666667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.3333333333%}.col-lg-pull-8{right:66.6666666667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.3333333333%}.col-lg-pull-11{right:91.6666666667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.3333333333%}.col-lg-push-2{left:16.6666666667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.3333333333%}.col-lg-push-5{left:41.6666666667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.3333333333%}.col-lg-push-8{left:66.6666666667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.3333333333%}.col-lg-push-11{left:91.6666666667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0%}.col-lg-offset-1{margin-left:8.3333333333%}.col-lg-offset-2{margin-left:16.6666666667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.3333333333%}.col-lg-offset-5{margin-left:41.6666666667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.3333333333%}.col-lg-offset-8{margin-left:66.6666666667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.3333333333%}.col-lg-offset-11{margin-left:91.6666666667%}.col-lg-offset-12{margin-left:100%}}table{background-color:rgba(0,0,0,0);color:#444}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:18px}.table>thead>tr>th,.table>thead>tr>td,.table>tbody>tr>th,.table>tbody>tr>td,.table>tfoot>tr>th,.table>tfoot>tr>td{padding:10px 0px 10px 20px;line-height:1.428571429;vertical-align:top;border-top:1px solid #eee}.table>thead>tr>th{vertical-align:bottom;border-bottom:1px solid #eee}.table>caption+thead>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>th,.table>thead:first-child>tr:first-child>td{border-top:0;font-family:"Inter";font-weight:normal}.table>tbody+tbody{border-top:2px solid #eee}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>tfoot>tr>td{padding:8px}.table-bordered{border:1px solid #eee}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>tfoot>tr>td{border:1px solid #eee}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#fbfbfb}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>thead>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>thead>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th{background-color:#7ebc59}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#70b348}.table>thead>tr>td.info,.table>thead>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>thead>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>thead>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th{background-color:#ee451f}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#e23811}@media screen and (max-width: 767px){.table-responsive{width:100%;margin-bottom:13.5px;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:18px;font-size:19.5px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:normal}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9 ;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:none}output{display:block;padding-top:7px;font-size:13px;line-height:1.428571429;color:#555}select,textarea,input[type=text],input[type=password],input[type=datetime],input[type=datetime-local],input[type=date],input[type=month],input[type=time],input[type=week],input[type=number],input[type=email],input[type=url],input[type=search],input[type=tel],input[type=color]{display:block;width:100%;height:32px;padding:6px 12px;font-size:13px;line-height:1.428571429;color:#555;background-color:#fff;background-image:none;border:1px solid #e4eaec;border-radius:3px;text-overflow:ellipsis;max-width:450px;transition:none;-webkit-transition:border-color ease-in-out .1s,box-shadow ease-in-out .1s;-o-transition:border-color ease-in-out .1s,box-shadow ease-in-out .1s;transition:border-color ease-in-out .1s,box-shadow ease-in-out .1s}select:focus,textarea:focus,input[type=text]:focus,input[type=password]:focus,input[type=datetime]:focus,input[type=datetime-local]:focus,input[type=date]:focus,input[type=month]:focus,input[type=time]:focus,input[type=week]:focus,input[type=number]:focus,input[type=email]:focus,input[type=url]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=color]:focus{border-color:#66afe9;outline:0;transition:border-color 250ms ease}select::-moz-placeholder,textarea::-moz-placeholder,input[type=text]::-moz-placeholder,input[type=password]::-moz-placeholder,input[type=datetime]::-moz-placeholder,input[type=datetime-local]::-moz-placeholder,input[type=date]::-moz-placeholder,input[type=month]::-moz-placeholder,input[type=time]::-moz-placeholder,input[type=week]::-moz-placeholder,input[type=number]::-moz-placeholder,input[type=email]::-moz-placeholder,input[type=url]::-moz-placeholder,input[type=search]::-moz-placeholder,input[type=tel]::-moz-placeholder,input[type=color]::-moz-placeholder{color:#777;opacity:1}select:-ms-input-placeholder,textarea:-ms-input-placeholder,input[type=text]:-ms-input-placeholder,input[type=password]:-ms-input-placeholder,input[type=datetime]:-ms-input-placeholder,input[type=datetime-local]:-ms-input-placeholder,input[type=date]:-ms-input-placeholder,input[type=month]:-ms-input-placeholder,input[type=time]:-ms-input-placeholder,input[type=week]:-ms-input-placeholder,input[type=number]:-ms-input-placeholder,input[type=email]:-ms-input-placeholder,input[type=url]:-ms-input-placeholder,input[type=search]:-ms-input-placeholder,input[type=tel]:-ms-input-placeholder,input[type=color]:-ms-input-placeholder{color:#777}select::-webkit-input-placeholder,textarea::-webkit-input-placeholder,input[type=text]::-webkit-input-placeholder,input[type=password]::-webkit-input-placeholder,input[type=datetime]::-webkit-input-placeholder,input[type=datetime-local]::-webkit-input-placeholder,input[type=date]::-webkit-input-placeholder,input[type=month]::-webkit-input-placeholder,input[type=time]::-webkit-input-placeholder,input[type=week]::-webkit-input-placeholder,input[type=number]::-webkit-input-placeholder,input[type=email]::-webkit-input-placeholder,input[type=url]::-webkit-input-placeholder,input[type=search]::-webkit-input-placeholder,input[type=tel]::-webkit-input-placeholder,input[type=color]::-webkit-input-placeholder{color:#777}select[disabled],select[readonly],fieldset[disabled] select,textarea[disabled],textarea[readonly],fieldset[disabled] textarea,input[type=text][disabled],input[type=text][readonly],fieldset[disabled] input[type=text],input[type=password][disabled],input[type=password][readonly],fieldset[disabled] input[type=password],input[type=datetime][disabled],input[type=datetime][readonly],fieldset[disabled] input[type=datetime],input[type=datetime-local][disabled],input[type=datetime-local][readonly],fieldset[disabled] input[type=datetime-local],input[type=date][disabled],input[type=date][readonly],fieldset[disabled] input[type=date],input[type=month][disabled],input[type=month][readonly],fieldset[disabled] input[type=month],input[type=time][disabled],input[type=time][readonly],fieldset[disabled] input[type=time],input[type=week][disabled],input[type=week][readonly],fieldset[disabled] input[type=week],input[type=number][disabled],input[type=number][readonly],fieldset[disabled] input[type=number],input[type=email][disabled],input[type=email][readonly],fieldset[disabled] input[type=email],input[type=url][disabled],input[type=url][readonly],fieldset[disabled] input[type=url],input[type=search][disabled],input[type=search][readonly],fieldset[disabled] input[type=search],input[type=tel][disabled],input[type=tel][readonly],fieldset[disabled] input[type=tel],input[type=color][disabled],input[type=color][readonly],fieldset[disabled] input[type=color]{cursor:not-allowed;background-color:#eee;opacity:1}textarea{height:auto}input[type=search]{-webkit-appearance:none}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:32px;line-height:1.428571429 \0 }input[type=date].input-sm,.input-group-sm>input[type=date].form-control,.input-group-sm>input[type=date].input-group-addon,.input-group-sm>.input-group-btn>input[type=date].btn,.form-horizontal .form-group-sm input[type=date].form-control,input[type=time].input-sm,.input-group-sm>input[type=time].form-control,.input-group-sm>input[type=time].input-group-addon,.input-group-sm>.input-group-btn>input[type=time].btn,.form-horizontal .form-group-sm input[type=time].form-control,input[type=datetime-local].input-sm,.input-group-sm>input[type=datetime-local].form-control,.input-group-sm>input[type=datetime-local].input-group-addon,.input-group-sm>.input-group-btn>input[type=datetime-local].btn,.form-horizontal .form-group-sm input[type=datetime-local].form-control,input[type=month].input-sm,.input-group-sm>input[type=month].form-control,.input-group-sm>input[type=month].input-group-addon,.input-group-sm>.input-group-btn>input[type=month].btn,.form-horizontal .form-group-sm input[type=month].form-control{line-height:30px}input[type=date].input-lg,.input-group-lg>input[type=date].form-control,.input-group-lg>input[type=date].input-group-addon,.input-group-lg>.input-group-btn>input[type=date].btn,.form-horizontal .form-group-lg input[type=date].form-control,input[type=time].input-lg,.input-group-lg>input[type=time].form-control,.input-group-lg>input[type=time].input-group-addon,.input-group-lg>.input-group-btn>input[type=time].btn,.form-horizontal .form-group-lg input[type=time].form-control,input[type=datetime-local].input-lg,.input-group-lg>input[type=datetime-local].form-control,.input-group-lg>input[type=datetime-local].input-group-addon,.input-group-lg>.input-group-btn>input[type=datetime-local].btn,.form-horizontal .form-group-lg input[type=datetime-local].form-control,input[type=month].input-lg,.input-group-lg>input[type=month].form-control,.input-group-lg>input[type=month].input-group-addon,.input-group-lg>.input-group-btn>input[type=month].btn,.form-horizontal .form-group-lg input[type=month].form-control{line-height:45px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:18px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{position:absolute;margin-left:-20px;margin-top:4px \9 }.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=radio].disabled,fieldset[disabled] input[type=radio],input[type=checkbox][disabled],input[type=checkbox].disabled,fieldset[disabled] input[type=checkbox]{cursor:not-allowed}.radio-inline.disabled,fieldset[disabled] .radio-inline,.checkbox-inline.disabled,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,fieldset[disabled] .radio label,.checkbox.disabled label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.form-horizontal .form-group-lg .form-control-static.form-control,.form-control-static.input-sm,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn,.form-horizontal .form-group-sm .form-control-static.form-control{padding-left:0;padding-right:0}.input-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn,.form-horizontal .form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,.input-group-sm>.input-group-btn>select.btn,.form-horizontal .form-group-sm select.form-control{height:30px;line-height:30px}textarea.input-sm,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,.input-group-sm>.input-group-btn>textarea.btn,.form-horizontal .form-group-sm textarea.form-control,select[multiple].input-sm,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>.input-group-btn>select[multiple].btn,.form-horizontal .form-group-sm select[multiple].form-control{height:auto}.input-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn,.form-horizontal .form-group-lg .form-control{height:45px;padding:10px 16px;font-size:17px;line-height:1.33;border-radius:6px}select.input-lg,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,.input-group-lg>.input-group-btn>select.btn,.form-horizontal .form-group-lg select.form-control{height:45px;line-height:45px}textarea.input-lg,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,.input-group-lg>.input-group-btn>textarea.btn,.form-horizontal .form-group-lg textarea.form-control,select[multiple].input-lg,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>.input-group-btn>select[multiple].btn,.form-horizontal .form-group-lg select[multiple].form-control{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:40px}.form-control-feedback{position:absolute;top:23px;right:0;z-index:2;display:block;width:32px;height:32px;line-height:32px;text-align:center}.input-lg+.form-control-feedback,.input-group-lg>.form-control+.form-control-feedback,.input-group-lg>.input-group-addon+.form-control-feedback,.input-group-lg>.input-group-btn>.btn+.form-control-feedback,.form-horizontal .form-group-lg .form-control+.form-control-feedback{width:45px;height:45px;line-height:45px}.input-sm+.form-control-feedback,.input-group-sm>.form-control+.form-control-feedback,.input-group-sm>.input-group-addon+.form-control-feedback,.input-group-sm>.input-group-btn>.btn+.form-control-feedback,.form-horizontal .form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#7ebc59}.has-success .form-control{border-color:#7ebc59;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#65a141;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #b6d9a2;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #b6d9a2}.has-success .input-group-addon{color:#7ebc59;border-color:#7ebc59;background-color:#7ebc59}.has-success .form-control-feedback{color:#7ebc59}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#ec971f;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f8d9ac;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f8d9ac}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#f0ad4e}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#ee451f}.has-error .form-control{border-color:#ee451f;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#cb320f;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f5947e;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f5947e}.has-error .input-group-addon{color:#ee451f;border-color:#ee451f;background-color:#ee451f}.has-error .form-control-feedback{color:#ee451f}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#7c7c7a}@media(min-width: 768px){.form-inline .form-group,.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control,.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group,.navbar-form .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.navbar-form .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.navbar-form .input-group .input-group-btn,.form-inline .input-group .form-control,.navbar-form .input-group .form-control{width:auto}.form-inline .input-group>.form-control,.navbar-form .input-group>.form-control{width:100%}.form-inline .control-label,.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.navbar-form .radio,.form-inline .checkbox,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.navbar-form .radio label,.form-inline .checkbox label,.navbar-form .checkbox label{padding-left:0}.form-inline .radio input[type=radio],.navbar-form .radio input[type=radio],.form-inline .checkbox input[type=checkbox],.navbar-form .checkbox input[type=checkbox]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback,.navbar-form .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:25px}.form-horizontal .form-group{margin-left:-20px;margin-right:-20px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{content:" ";display:table}.form-horizontal .form-group:after{clear:both}@media(min-width: 768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .has-feedback control-feedback{top:0;right:20px}@media(min-width: 768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media(min-width: 768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid rgba(0,0,0,0);white-space:nowrap;padding:6px 12px;font-size:13px;line-height:1.428571429;border-radius:3px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#fff;background-color:#536270;border-color:#536270}.btn:hover,.btn:focus,.btn:active,.btn.active,.open>.btn.dropdown-toggle{color:#fff;background-color:#3d4853;border-color:#39434d}.btn:active,.btn.active,.open>.btn.dropdown-toggle{background-image:none}.btn.disabled,.btn.disabled:hover,.btn.disabled:focus,.btn.disabled:active,.btn.disabled.active,.btn[disabled],.btn[disabled]:hover,.btn[disabled]:focus,.btn[disabled]:active,.btn[disabled].active,fieldset[disabled] .btn,fieldset[disabled] .btn:hover,fieldset[disabled] .btn:focus,fieldset[disabled] .btn:active,fieldset[disabled] .btn.active{background-color:#536270;border-color:#536270}.btn .badge{color:#536270;background-color:#fff}.btn:focus,.btn:active:focus,.btn.active:focus{outline:none}.btn:hover,.btn:focus{color:#fff;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#fff;background-color:#536270;border-color:#536270}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{color:#fff;background-color:#3d4853;border-color:#39434d}.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{background-image:none}.btn-default.disabled,.btn-default.disabled:hover,.btn-default.disabled:focus,.btn-default.disabled:active,.btn-default.disabled.active,.btn-default[disabled],.btn-default[disabled]:hover,.btn-default[disabled]:focus,.btn-default[disabled]:active,.btn-default[disabled].active,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default:hover,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default.active{background-color:#536270;border-color:#536270}.btn-default .badge{color:#536270;background-color:#fff}.btn-primary{color:#fff;background-color:#51aded;border-color:#51aded}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#2397e8;border-color:#1a93e7}.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{background-image:none}.btn-primary.disabled,.btn-primary.disabled:hover,.btn-primary.disabled:focus,.btn-primary.disabled:active,.btn-primary.disabled.active,.btn-primary[disabled],.btn-primary[disabled]:hover,.btn-primary[disabled]:focus,.btn-primary[disabled]:active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary:hover,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary.active{background-color:#51aded;border-color:#51aded}.btn-primary .badge{color:#51aded;background-color:#fff}.btn-success{color:#fff;background-color:#7ebc59;border-color:#70b348}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#65a141;border-color:#558837}.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{background-image:none}.btn-success.disabled,.btn-success.disabled:hover,.btn-success.disabled:focus,.btn-success.disabled:active,.btn-success.disabled.active,.btn-success[disabled],.btn-success[disabled]:hover,.btn-success[disabled]:focus,.btn-success[disabled]:active,.btn-success[disabled].active,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success:hover,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success.active{background-color:#7ebc59;border-color:#70b348}.btn-success .badge{color:#7ebc59;background-color:#fff}.btn-info{color:#fff;background-color:#368cbf;border-color:#307dab}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#2b6f97;border-color:#235a7b}.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{background-image:none}.btn-info.disabled,.btn-info.disabled:hover,.btn-info.disabled:focus,.btn-info.disabled:active,.btn-info.disabled.active,.btn-info[disabled],.btn-info[disabled]:hover,.btn-info[disabled]:focus,.btn-info[disabled]:active,.btn-info[disabled].active,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info:hover,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info.active{background-color:#368cbf;border-color:#307dab}.btn-info .badge{color:#368cbf;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{background-image:none}.btn-warning.disabled,.btn-warning.disabled:hover,.btn-warning.disabled:focus,.btn-warning.disabled:active,.btn-warning.disabled.active,.btn-warning[disabled],.btn-warning[disabled]:hover,.btn-warning[disabled]:focus,.btn-warning[disabled]:active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning:hover,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#ee451f;border-color:#e23811}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#cb320f;border-color:#a92a0d}.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled,.btn-danger.disabled:hover,.btn-danger.disabled:focus,.btn-danger.disabled:active,.btn-danger.disabled.active,.btn-danger[disabled],.btn-danger[disabled]:hover,.btn-danger[disabled]:focus,.btn-danger[disabled]:active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger:hover,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger.active{background-color:#ee451f;border-color:#e23811}.btn-danger .badge{color:#ee451f;background-color:#fff}.btn-link{color:#51aded;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:rgba(0,0,0,0);-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:rgba(0,0,0,0)}.btn-link:hover,.btn-link:focus{color:#178adb;text-decoration:underline;background-color:rgba(0,0,0,0)}.btn-link[disabled]:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:hover,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:17px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid rgba(0,0,0,0);border-left:4px solid rgba(0,0,0,0)}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:13px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:3px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:8px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#51aded}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:rgba(0,0,0,0);background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width: 768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar:before,.btn-toolbar:after{content:" ";display:table}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle,.btn-group-lg.btn-group>.btn+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret,.btn-group-lg>.btn .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret,.dropup .btn-group-lg>.btn .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{content:" ";display:table}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:3px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:3px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{position:absolute;z-index:-1;opacity:0;filter:alpha(opacity=0)}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:13px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #e4eaec;border-radius:3px}.input-group-addon.input-sm,.form-horizontal .form-group-sm .input-group-addon.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.form-horizontal .form-group-lg .input-group-addon.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:17px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav:before,.nav:after{content:" ";display:table}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;background-color:rgba(0,0,0,0);cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#51aded}.nav .nav-divider{height:1px;margin:8px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #e5e5e5}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid rgba(0,0,0,0);border-radius:3px 3px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #e5e5e5}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:rgba(0,0,0,0);cursor:default}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#51aded}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified,.nav-tabs.nav-justified{width:100%}.nav-justified>li,.nav-tabs.nav-justified>li{float:none}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{text-align:left;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width: 768px){.nav-justified>li,.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified,.nav-tabs.nav-justified{border-bottom:0}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:3px}.nav-tabs-justified>.active>a,.nav-tabs.nav-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width: 768px){.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:3px 3px 0 0}.nav-tabs-justified>.active>a,.nav-tabs.nav-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:0;border:1px solid rgba(0,0,0,0)}.navbar:before,.navbar:after{content:" ";display:table}.navbar:after{clear:both}@media(min-width: 768px){.navbar{border-radius:0}}.navbar-header:before,.navbar-header:after{content:" ";display:table}.navbar-header:after{clear:both}@media(min-width: 768px){.navbar-header{float:left}}.navbar-collapse{overflow-x:visible;padding-right:20px;padding-left:20px;border-top:1px solid rgba(0,0,0,0);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{content:" ";display:table}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width: 768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media(max-width: 480px)and (orientation: landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-header,.container-fluid>.navbar-collapse{margin-right:-20px;margin-left:-20px}@media(min-width: 768px){.container>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-header,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width: 768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}@media(min-width: 768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 20px;font-size:17px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width: 768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-20px}}.navbar-toggle{position:relative;float:left;margin-right:20px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:rgba(0,0,0,0);background-image:none;border:1px solid rgba(0,0,0,0);border-radius:3px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width: 768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -20px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:18px}@media(max-width: 767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:rgba(0,0,0,0);border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:18px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width: 768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-20px}}@media(min-width: 768px){.navbar-left{float:left !important}.navbar-right{float:right !important}}.navbar-form{margin-left:-20px;margin-right:-20px;padding:10px 0px;border-top:1px solid rgba(0,0,0,0);border-bottom:1px solid rgba(0,0,0,0);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:9px;margin-bottom:9px}@media(max-width: 767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width: 768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-20px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:9px;margin-bottom:9px}.navbar-btn.btn-sm,.btn-group-sm>.navbar-btn.btn{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs,.btn-group-xs>.navbar-btn.btn{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:16px;margin-bottom:16px}@media(min-width: 768px){.navbar-text{float:left;margin-left:20px;margin-right:20px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#3c3c3b;border-color:#2b2b2b}.navbar-default .navbar-brand{color:#f7f7f7}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#dedede;background-color:rgba(0,0,0,0)}.navbar-default .navbar-text{color:#f7f7f7}.navbar-default .navbar-nav>li>a{color:#f7f7f7}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{background-color:rgba(0,0,0,0)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#2b2b2b}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:rgba(0,0,0,0)}.navbar-default .navbar-toggle{border-color:#fff}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#555}.navbar-default .navbar-toggle .icon-bar{background-color:#fff}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#2b2b2b}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#2b2b2b;color:#555}@media(max-width: 767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#f7f7f7}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{background-color:rgba(0,0,0,0)}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#2b2b2b}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:rgba(0,0,0,0)}}.navbar-default .navbar-link{color:#f7f7f7}.navbar-default .btn-link{color:#f7f7f7}.navbar-default .btn-link[disabled]:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:hover,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#090909}.navbar-inverse .navbar-brand{color:#777}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:rgba(0,0,0,0)}.navbar-inverse .navbar-text{color:#777}.navbar-inverse .navbar-nav>li>a{color:#777}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:rgba(0,0,0,0)}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:rgba(0,0,0,0)}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#090909;color:#fff}@media(max-width: 767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:rgba(0,0,0,0)}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:rgba(0,0,0,0)}}.navbar-inverse .navbar-link{color:#777}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#777}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:hover,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:18px;list-style:none;background-color:#f5f5f5;border-radius:3px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/ ";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:18px 0;border-radius:3px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.428571429;text-decoration:none;color:#51aded;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pagination>li>a:hover,.pagination>li>a:focus,.pagination>li>span:hover,.pagination>li>span:focus{color:#178adb;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:hover,.pagination>.active>a:focus,.pagination>.active>span,.pagination>.active>span:hover,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#51aded;border-color:#51aded;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:17px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:18px 0;list-style:none;text-align:center}.pager:before,.pager:after{content:" ";display:table}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label:empty{display:none}.btn .label{position:relative;top:-1px}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#51aded}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#2397e8}.label-success{background-color:#7ebc59}.label-success[href]:hover,.label-success[href]:focus{background-color:#65a141}.label-info{background-color:#368cbf}.label-info[href]:hover,.label-info[href]:focus{background-color:#2b6f97}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#ee451f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#cb320f}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#51aded;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:20px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width: 768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:58.5px}}.thumbnail{display:block;padding:4px;margin-bottom:18px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:3px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;width:100% \9 ;max-width:100%;height:auto;margin-left:auto;margin-right:auto}.thumbnail .caption{padding:9px;color:#3c3c3b}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#51aded}.alert{padding:15px;margin-bottom:18px;border:1px solid rgba(0,0,0,0);border-radius:3px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#7ebc59;border-color:#7ebc59;color:#4e7d32}.alert-success hr{border-top-color:#70b348}.alert-success .alert-link{color:#598f3a}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#173543}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#1d4356}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#c77c11}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#df8a13}.alert-danger{background-color:#ee451f;border-color:#ee451f;color:#9b260c}.alert-danger hr{border-top-color:#e23811}.alert-danger .alert-link{color:#b32c0e}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:18px;background-color:#f5f5f5;border-radius:3px;position:relative;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:18px;color:#fff;text-align:center;background-color:#51aded;position:relative;z-index:2;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar[aria-valuenow="1"],.progress-bar[aria-valuenow="2"]{min-width:30px}.progress-bar[aria-valuenow="0"]{color:#777;min-width:30px;background-color:rgba(0,0,0,0);background-image:none;box-shadow:none}.progress-bar-success{background-color:#7ebc59}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#368cbf}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#ee451f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:6px 8px;margin-bottom:-1px;background-color:#fff}.list-group-item:last-child{margin-bottom:0}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#3c3c3b;border-radius:0}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item:hover:before,a.list-group-item:focus:before{background:#3498db;content:"";height:42px;left:0;position:absolute;top:0;width:3px}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eee;color:#777}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2}.list-group-item.active:before,.list-group-item.active:hover:before,.list-group-item.active:focus:before{background:#3498db;content:"";height:42px;left:0;position:absolute;top:0px;width:3px}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#fff}.list-group-item.active+.collapse>.list-group-item:before,.list-group-item.active:hover+.collapse>.list-group-item:before,.list-group-item.active:focus+.collapse>.list-group-item:before{background:#3498db;content:"";height:42px;left:0;position:absolute;top:0px;width:3px}.list-group-item-success{color:#7ebc59;background-color:#7ebc59}a.list-group-item-success{color:#7ebc59}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#7ebc59;background-color:#70b348}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#7ebc59;border-color:#7ebc59}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#f0ad4e;background-color:#fcf8e3}a.list-group-item-warning{color:#f0ad4e}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#f0ad4e;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.list-group-item-danger{color:#ee451f;background-color:#ee451f}a.list-group-item-danger{color:#ee451f}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#ee451f;background-color:#e23811}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#ee451f;border-color:#ee451f}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:18px;background-color:#fff;border:1px solid rgba(0,0,0,0);border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{content:" ";display:table}.panel-body:after{clear:both}.panel-heading{padding:10px 15px;border-bottom:1px solid rgba(0,0,0,0);border-top-right-radius:2px;border-top-left-radius:2px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:15px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:2px;border-top-left-radius:2px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:2px;border-top-left-radius:2px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:2px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:2px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:2px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:2px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #eee}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:18px}.panel-group .panel{margin-bottom:0;border-radius:3px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#51aded}.panel-primary>.panel-heading{color:#fff;background-color:#51aded;border-color:#51aded}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#51aded}.panel-primary>.panel-heading .badge{color:#51aded;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#51aded}.panel-success{border-color:#7ebc59}.panel-success>.panel-heading{color:#7ebc59;background-color:#7ebc59;border-color:#7ebc59}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#7ebc59}.panel-success>.panel-heading .badge{color:#7ebc59;background-color:#7ebc59}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#7ebc59}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#f0ad4e;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#f0ad4e}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ee451f}.panel-danger>.panel-heading{color:#ee451f;background-color:#ee451f;border-color:#ee451f}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ee451f}.panel-danger>.panel-heading .badge{color:#ee451f;background-color:#ee451f}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ee451f}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:3px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:19.5px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:rgba(0,0,0,0);border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate3d(0, -25%, 0);transform:translate3d(0, -25%, 0);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:62px 10px 10px 10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.428571429px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{content:" ";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media(min-width: 768px){.modal-dialog{width:600px;margin:82px auto 30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media(min-width: 992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:3px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:rgba(0,0,0,0);border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:13px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:rgba(0,0,0,0);border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;width:100% \9 ;max-width:100%;height:auto;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);background-image:-o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);background-image:linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000", endColorstr="#00000000", GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);background-image:-o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);background-image:linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#00000000", endColorstr="#80000000", GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif}.carousel-control .icon-prev:before{content:"‹"}.carousel-control .icon-next:before{content:"›"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9 ;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width: 768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.content-box:before,.clearfix:after,.content-box:after{content:" ";display:table}.clearfix:after,.content-box:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:rgba(0,0,0,0);text-shadow:none;background-color:rgba(0,0,0,0);border:0}.hidden{display:none !important;visibility:hidden !important}.affix{position:fixed;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media(max-width: 767px){.visible-xs{display:block !important}table.visible-xs{display:table}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media(max-width: 767px){.visible-xs-block{display:block !important}}@media(max-width: 767px){.visible-xs-inline{display:inline !important}}@media(max-width: 767px){.visible-xs-inline-block{display:inline-block !important}}@media(min-width: 768px)and (max-width: 991px){.visible-sm{display:block !important}table.visible-sm{display:table}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media(min-width: 768px)and (max-width: 991px){.visible-sm-block{display:block !important}}@media(min-width: 768px)and (max-width: 991px){.visible-sm-inline{display:inline !important}}@media(min-width: 768px)and (max-width: 991px){.visible-sm-inline-block{display:inline-block !important}}@media(min-width: 992px)and (max-width: 1199px){.visible-md{display:block !important}table.visible-md{display:table}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media(min-width: 992px)and (max-width: 1199px){.visible-md-block{display:block !important}}@media(min-width: 992px)and (max-width: 1199px){.visible-md-inline{display:inline !important}}@media(min-width: 992px)and (max-width: 1199px){.visible-md-inline-block{display:inline-block !important}}@media(min-width: 1200px){.visible-lg{display:block !important}table.visible-lg{display:table}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media(min-width: 1200px){.visible-lg-block{display:block !important}}@media(min-width: 1200px){.visible-lg-inline{display:inline !important}}@media(min-width: 1200px){.visible-lg-inline-block{display:inline-block !important}}@media(max-width: 767px){.hidden-xs,.page-side{display:none !important}}@media(min-width: 768px)and (max-width: 991px){.hidden-sm{display:none !important}}@media(max-width: 768px),(max-height: 669px){.toggle-sidebar{display:none !important}}@media(min-width: 992px)and (max-width: 1199px){.hidden-md{display:none !important}}@media(min-width: 1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}*{-webkit-font-smoothing:antialiased}html,body{font-family:"Inter",serif;font-weight:400;height:100%;background-color:#fff}body{min-width:320px;touch-action:manipulation}.page-head{position:fixed;z-index:2;top:0;right:0;left:0;background:#3c3c3b}.page-head .navbar-default{min-height:60px;color:rgba(87,87,87,.67);border:0;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.08)}.page-head .navbar-text{color:#494949}.page-head .navbar-right{margin-top:6px}.page-head .navbar-brand{display:flex;flex-direction:column;width:250px;height:100%;text-align:center;color:#000;background:#3498db}.page-head .navbar-brand::before{font-size:16px;font-weight:500;display:inline-block;content:"OPNSense";color:#fff}.page-head .navbar-brand::after{font-size:12px;font-weight:400;display:inline-block;content:"Advanced Theme";color:#fafafa}.page-head .navbar-brand:hover,.page-head .navbar-brand:focus{background:#3498db}.page-head .navbar-brand .brand-logo,.page-head .navbar-brand .brand-icon{width:1px;height:1px}.page-head .navbar-toggle{margin-top:14px}.page-head .navbar-toggle .icon-bar{background-color:#363636}.page-head .navbar-toggle:hover,.page-head .navbar-toggle:focus{background-color:rgba(0,0,0,0)}.page-head .navbar-toggle:hover .icon-bar,.page-head .navbar-toggle:focus .icon-bar{background-color:#3498db}.page-content{position:relative;z-index:1;height:calc(100% - 60px);padding-top:60px}.page-content>.row{height:100%}@media(min-width: 768px){.page-content.col-lg-10,.page-content.col-sm-9{width:calc(100% - 250px)}.page-content.col-lg-push-2,.page-content.col-sm-push-3{left:250px}}.page-content-head,.content-box-head{padding-top:20px;padding-bottom:15px;border-bottom:1px solid rgba(217,217,217,.5);background:#fbfbfb}.page-content-head .navbar-nav,.content-box-head .navbar-nav{width:100%}.page-content-head h1,.content-box-head h1,.page-content-head h2,.content-box-head h2,.page-content-head h3,.content-box-head h3{line-height:1;margin:0}.page-content-main{min-height:calc(100% - 64px);padding:15px 0 73px;background:#f7f7f7}.page-side{position:fixed;z-index:3;top:0;left:0;overflow:auto;width:250px;height:100% !important;height:calc(100% - 60px) !important;margin-top:60px;border-right:1px solid rgba(0,0,0,.15);background:#263238}.page-side-nav--active{border-left:3px solid;background:#f7f7f7}.page-side-nav .panel{background:#263238}.page-foot{font-size:12px;position:fixed;z-index:2;bottom:0;width:100%;padding:20px 0;border-top:1px solid rgba(217,217,217,.5);background:#fbfbfb}.content-box{border:1px solid #eaeaea;border-radius:3px;background:#fff}.content-box-main{padding-top:15px;padding-bottom:15px}div.content-box.wizard div img{filter:invert(25%)}.tab-content{padding:0px 0;border-top:0px}.tab-content>.tab-content{margin-bottom:0;padding:0 15px}.tab-content .tab-content:last-child{margin-bottom:0}.page-content-main section[class^=col-]+section[class^=col-]{padding-top:20px}.brand-logo{display:none}@media(min-width: 768px){.brand-logo{display:inline-block}}.brand-icon{display:inline-block}@media(min-width: 768px){.brand-icon{display:none}}@media(min-width: 768px){.col-sm-disable-spacer{padding-top:0 !important}}@media(min-width: 992px){.col-md-disable-spacer{padding-top:0 !important}}@media(min-width: 1200px){.col-lg-disable-spacer{padding-top:0 !important}}.page-login{background:linear-gradient(#eff2f5, #f5f7f8)}.page-login .container{display:flex;align-items:center;flex-direction:column;justify-content:center;min-height:100%}.page-login .container:after{height:60px}.login-foot{font-size:12px}.login-modal-container{width:100%;max-width:400px;margin-bottom:8px;border-radius:4px;background:#fff;box-shadow:0 1px 8px rgba(0,0,0,.1)}.login-modal-head{height:50px;padding:0 20px;border-top-left-radius:4px;border-top-right-radius:4px;background:#51aded}.login-modal-head img{display:none}.login-modal-head .navbar-brand{display:flex;float:none;align-items:center;flex-direction:column;justify-content:center;text-align:center}.login-modal-head .navbar-brand::before{font-size:16px;font-weight:500;display:inline-block;content:"OPNSense";color:#fff}.login-modal-head .navbar-brand::after{font-size:11px;font-weight:400;display:inline-block;content:"AdvancedOPN Theme";color:#fafafa}.login-modal-content{padding:20px 20px 20px 20px}.login-modal-foot{height:60px;padding:20px 20px 0 20px;border-top:1px solid #eaeaea;background:#f7f7f7}.login-modal-foot a{text-decoration:none;color:#7d7d7d}.login-modal-foot a:hover{text-decoration:underline;color:#646464}@media(min-width: 768px){.list-inline .btn-group-container{float:right}}.btn.btn-fixed{width:100%;max-width:174px}.progress-bar-placeholder{font-size:12px;position:absolute;z-index:1;width:100%;text-align:center}.list-group-item{border-right:none;border-left:none}.list-group-item.collapsed .caret{border-top:0;border-bottom:4px solid green}main.page-content.col-lg-12{padding-left:90px}#navigation .panel.list-group .fa:not(.__iconspacer){width:auto;padding-right:10px}#navigation .panel.list-group a.list-group-item{font-size:14px;position:relative;padding:12px 16px;text-decoration:none;color:#a6adb3;background:#263238}#navigation .panel.list-group a.list-group-item:hover{color:#e1e4e6;background:rgba(255,255,255,.03)}#navigation .panel.list-group a.list-group-item:hover::before{background-color:#3498db}#navigation .panel.list-group a.list-group-item.active{color:#e1e4e6;background:rgba(255,255,255,.02)}#navigation .panel.list-group>.list-group-item:not(.collapsed).active-menu-title::before{width:3px;transition:opacity 200ms;background-color:#3498db}#navigation .panel.list-group>div .list-group-item{font-size:13px;position:relative;padding:5px 0 5px 35px}#navigation .panel.list-group>div .list-group-item::before{position:absolute;z-index:1;top:-11px;bottom:13px;left:12px;display:block;width:0;content:"";opacity:.8;border-left:1px dotted #3e474e !important}#navigation .panel.list-group>div .list-group-item::after{position:absolute;z-index:1;top:13px;left:12px;display:block;width:12px;content:"";border-bottom:1px dotted #3e474e !important}#navigation .panel.list-group>div .list-group-item:hover::before,#navigation .panel.list-group>div .list-group-item.active::before,#navigation .panel.list-group>div .list-group-item:focus::before{opacity:1;background-color:#3e474e}#navigation.col-sidebar-left{overflow:hidden;width:70px;border:none !important;background-color:rgba(0,0,0,0) !important}#navigation.col-sidebar-left>div.row{height:100% !important}#navigation.col-sidebar-left>div.row>nav.page-side-nav{width:70px;height:100% !important;border-right:1px solid #304047;background:#263238}#navigation.col-sidebar-left>div>nav>#mainmenu>div>a.list-group-item{font-size:13px;width:70px;height:70px;padding-top:12px;padding-right:0px;padding-left:0px;text-align:center;color:#a6adb3;border-right:1px solid #304047;border-bottom:2px solid #lightergrey}#navigation.col-sidebar-left>div>nav>#mainmenu>div>a.list-group-item>span.fa,#navigation.col-sidebar-left>div>nav>#mainmenu>div>a.list-group-item>span.glyphicon{font-size:20px;visibility:visible}#navigation.col-sidebar-left>div>nav>#mainmenu>div>a.list-group-item>span.__iconspacer{display:block;height:30px;padding:0px;text-align:center}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapsing>a.list-group-item{font-size:14px !important;position:absolute !important;left:70px !important;display:block !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapsing>a.list-group-item{font-size:14px !important;position:absolute !important;left:166px !important;display:block !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>a.list-group-item,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapse.in>a.list-group-item{background-color:#263238 !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapse.in>a.list-group-item{font-size:14px !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>div.collapse>a.list-group-item,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>a.list-group-item{font-size:14px !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>div.collapse>a.list-group-item::before,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>div.collapse>a.list-group-item::after,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>a.list-group-item::before,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>a.list-group-item::after{display:none}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapsed>a.list-group-item{font-size:14px !important;padding-left:10px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>a.list-group-item,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse>div.collapse>a.list-group-item{padding:3px 8px !important}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in{font-size:14px;position:absolute;z-index:10;left:70px;width:168px;margin-top:-70px;border:1px solid #304047;-moz-box-shadow:2px 2px 1px 0px rgba(234,234,234,.5);-webkit-box-shadow:2px 2px 1px 0px rgba(234,234,234,.5);box-shadow:2px 2px 1px 0px rgba(234,234,234,.5)}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapse.in{font-size:14px;position:absolute;z-index:10;left:166px;width:168px;margin-top:-26px;border:1px solid #304047;-moz-box-shadow:2px 2px 1px 0px rgba(234,234,234,.5);-webkit-box-shadow:2px 2px 1px 0px rgba(234,234,234,.5);box-shadow:2px 2px 1px 0px rgba(234,234,234,.5)}#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapsing,#navigation.col-sidebar-left>div>nav>#mainmenu>div>div.collapse.in>div.collapsing{display:none}button.toggle-sidebar{font-size:14px;float:left;margin-top:20px;color:#575757;border:none;outline:none;background-color:rgba(0,0,0,0)}#navigation.collapse.in{display:block !important}.list-group-submenu .list-group-item:last-child,.collapse .list-group-item:last-child{border-bottom:none}ul.nav>li.dropdown>ul.dropdown-menu>li>a{padding:3px 10px}.nav-tabs{margin-right:1px;border-bottom:2px solid #eaeaea}.nav-tabs>li{margin-right:2px;border-radius:0px;border-top-right-radius:10px}.nav-tabs>li>a{margin-right:0px;cursor:pointer;color:#777;border-top-left-radius:4px;border-top-right-radius:4px;background:#fcfcfc}.nav-tabs>li>a{border:none;background:rgba(0,0,0,0)}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-bottom:2px solid #e0e0e0;background-color:#efefef}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#51aded;border-top:none;border-right:none;border-bottom:2px solid #51aded;border-left:none;background:rgba(0,0,0,0)}.nav-tabs>li>a.visible-lg-inline-block:not(.pull-right){padding-right:6px !important;padding-left:12px !important;border-top-right-radius:0px !important}.nav-tabs>li>a.visible-lg-inline-block.pull-right{padding-right:12px !important;padding-left:6px !important;border-left:0px !important;border-top-left-radius:0}.nav-tabs.nav-justified{border-right:1px solid #eaeaea}.nav-tabs.nav-justified>li{border-top:1px solid #eaeaea;border-bottom:1px solid #eaeaea;border-left:1px solid #eaeaea;border-radius:0px;background:#f7f7f7}.nav-tabs.nav-justified>li>a{font-family:"Inter",serif;color:#3c3c3b}@media(min-width: 768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid rgba(0,0,0,0)}}@media(max-width: 767px){.nav-tabs.nav-justified>li.active>a{border-right:0 !important}}@media(min-width: 768px){>li.active+li>a{border-left:1px solid rgba(0,0,0,0)}}>li:last-child>a{border-right:1px solid rgba(0,0,0,0) !important}@media(max-width: 767px){>li:last-child>a{margin-bottom:0}}#opn-status-list .btn{color:#565656;border:1px solid #ecedf1;background:rgba(0,0,0,0)}#opn-status-list .btn:hover{background:#f9f9f9}.bootstrap-dialog-title{font-size:15px;font-weight:500}.bootstrap-select .btn-default{color:#76838f;border:1px solid #e4eaec;background:#fff}.bootstrap-select .btn-default:focus{border-color:#66afe9;background:#fff}.bootstrap-select.open>.btn-default{color:#76838f;border-color:#66afe9;background:#fff}.bootstrap-select .dropdown-toggle:focus{outline-color:#fff !important}.btn .glyphicon{vertical-align:-1px}.btn-default .glyphicon{color:#757575}table{width:100%}.table{margin-bottom:0px !important}.nav-tabs-justified .nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified .nav-tabs.nav-justified>.active>a:focus{border:0px !important}.table th,strong,b{font-family:"Inter",serif;font-weight:500}.table>tbody>tr>td:last-child{padding-right:15px}.__nowrap{white-space:nowrap}.__nomb{margin-bottom:0}.__mb{margin-bottom:15px}.__mt{margin-top:15px}.__ml{margin-left:15px}.__mr{margin-right:15px}.__iconspacer{padding-right:10px}#mainmenu .glyphicon{vertical-align:-2px}.list-group-item{overflow:hidden;text-overflow:ellipsis}.list-group-item+div.collapse{margin-bottom:-1px}.list-group-item+div>a{padding-left:44px}.list-group-item:before{position:absolute;top:0px;left:0;width:0;height:42px;min-height:100%;content:"";transition:opacity 0ms;opacity:0}.list-group-submenu a{padding-left:56px}.active-menu-title,.active-menu a{position:relative;text-decoration:none;background-color:#242f35}.active-menu-title:before,.active-menu a:before{width:3px}.active-menu-title.active,.active-menu a.active{background-color:#242f35}.active-menu a:before{background:#242f35}.alert.alert-danger{color:#fff !important}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{border-radius:0 !important}.navbar-brand{padding:10px 20px}.label-opnsense{font-size:12px;line-height:1.5;display:inline-block;vertical-align:middle;border:1px solid rgba(0,0,0,0);border-radius:3px}.label-opnsense-sm{padding:5px 10px}.label-opnsense-xs,.btn-xs,.btn-group-xs>.btn{padding:1px 3px}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-button{width:8px;height:5px}::-webkit-scrollbar-track{border-radius:0;background:#dcdcdc;box-shadow:0px 0px 0px}::-webkit-scrollbar-thumb{border:thin solid #e5e5e5;border-radius:0px;background:#afafaf}::-webkit-scrollbar-thumb:hover{background:#e5e5e5}.widgetdiv{padding-top:0px !important;padding-bottom:20px}select{overflow:hidden;cursor:pointer;border:1px solid #ccc;background-image:url("/ui/themes/advanced/build/images/caret.png") !important;background-repeat:no-repeat;background-position:right;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media screen and (max-width: 767px){.table-responsive{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>tfoot>tr>td{white-space:normal}}label>input[type=checkbox],label>input[type=radio]{float:left;margin-right:.4em}input[type=checkbox],input[type=radio]{font:inherit;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin:0;cursor:pointer;transition:border-color 150ms ease,background-color 150ms ease;transform:translateY(1px);color:#3f3f3f;border:1px solid #e4eaec;border-radius:.15em;background-color:#fff;-webkit-appearance:none;appearance:none}input[type=checkbox]::before,input[type=radio]::before{visibility:hidden;width:10px;height:10px;content:"";transition:transform 120ms ease-out 60ms,opacity 120ms ease-out,visibility 120ms ease-out;transform:scale(0.65);opacity:0;background-color:#fafafa;box-shadow:inset 1em 1em var(--form-control-color);clip-path:polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%)}input[type=checkbox]:checked,input[type=radio]:checked{border-color:#3498db;background-color:#3498db}input[type=checkbox]:checked::before,input[type=radio]:checked::before{visibility:visible;transform:scale(1);opacity:1;background-color:#fff}input[type=checkbox]:hover,input[type=radio]:hover{border-color:#3498db}input[type=checkbox]:disabled,input[type=radio]:disabled{cursor:not-allowed;color:var(--form-control-disabled);--form-control-color: var(--form-control-disabled)}input[type=radio]{border-radius:50%}div[data-for*=help_for]{font-size:.85em;font-style:italic;padding-top:.5em}#log-settings label[for^=act]{margin-right:1.5em}#log-settings table>tbody>tr>td{vertical-align:middle}#log-settings select#filterlogentries,#log-settings select#filterlogentriesupdateinterval{width:5em}#log-settings select#filterlogentriesinterfaces{min-width:100%;max-width:100%}[data-state=lightcoral]{background-color:#f08080}[data-state=white]{background-color:#fff}[data-state=red]{background-color:red}.tokens-container{margin-top:0px;margin-bottom:0px}.actionBar .btn.btn-default{color:#767676;border-color:#d8d8d8;background:#fafafa}.bootgrid-table td.select-cell{overflow:visible;width:30px !important}.act_toggle{font-size:15px}.fa.command-toggle{font-size:16px;padding-top:5px;vertical-align:middle}.form-control:hover{border-color:#d1e5f2}.table input[type=checkbox]{margin-top:-4px;margin-bottom:-4px}.table .btn-xs,.table .btn-group-xs>.btn{margin-top:-4px;margin-bottom:-4px}.widget-sort-handle{touch-action:none}.bootgrid-table{table-layout:auto !important}table#tunable.bootgrid-table td{white-space:normal !important}@media(min-width: 1200px){table#grid-leases tr td:nth-child(3){width:15% !important}}#grid-log th[data-column-id=__timestamp__],#filter-log-entries th[data-column-id=__timestamp__]{min-width:3.5em}/*# sourceMappingURL=main.css.map */ diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/opnsense-bootgrid.css b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/opnsense-bootgrid.css new file mode 100644 index 000000000..4d353b768 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/css/opnsense-bootgrid.css @@ -0,0 +1 @@ +.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover{background-color:#fbfbfb}.tabulator .tabulator-headers{height:100% !important}.tabulator .tabulator-tableholder .tabulator-table{background-color:rgba(0,0,0,0)}.tabulator-row{background-color:rgba(0,0,0,0);color:#373736}.tabulator-row.tabulator-row-odd{background-color:#fbfbfb}.tabulator-row.tabulator-row-odd.tabulator-selectable:hover:not(.tabulator-selected){background-color:#f5f5f5}.tabulator-row.tabulator-row-even.tabulator-selectable:hover:not(.tabulator-selected){background-color:#f5f5f5}.tabulator .tabulator-tableholder{background-color:rgba(0,0,0,0)}.tabulator-row.tabulator-selected{background-color:#e3e8f0}.tabulator-row.tabulator-selected:hover{cursor:pointer;background-color:#e3e8f0}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg{color:#373736}.tabulator-row.tabulator-group{border:none;pointer-events:none}.tabulator-row.tabulator-group span{margin-left:0;color:#373736}.tabulator-row .badge.chip{background-color:#31708f;color:#fff;font-size:10px;padding:2px 4px;position:relative;top:-1px}.tabulator-row.tabulator-group .tabulator-group-toggle,.tabulator-row.tabulator-group .tabulator-group-toggle *{pointer-events:auto}.tabulator-row.tabulator-group:has(>.tabulator-group-toggle:only-child){display:none}.tabulator-row .tabulator-cell{text-overflow:ellipsis;white-space:nowrap;line-height:1.2;vertical-align:top}.tabulator .tabulator-footer{border-top:none;background-color:rgba(0,0,0,0)}.tabulator-page-counter{color:#373736}.tabulator .tabulator-footer .tabulator-paginator{color:#373736}.tabulator-paginator{flex:0 !important}.tabulator-page-counter{flex:1;text-align:right}.tabulator-col-sorter i{display:flex;justify-content:center;align-items:center}.tabulator .tabulator-header .tabulator-col{padding:5px}.tabulator .tabulator-headers>:first-child{padding-left:10px}.opnsense-bootgrid-responsive{white-space:wrap !important;text-overflow:inherit !important;overflow:visible !important;word-break:break-word !important}.opnsense-bootgrid-ellipsis{overflow:hidden !important;white-space:nowrap !important;text-overflow:ellipsis !important}.tabulator-row>:first-child{padding-left:10px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content{padding:0px}.tabulator-page-counter{padding-right:20px}.bootgrid-footer-commands{width:90px;padding-left:8px}.tabulator-tableholder::after{content:"";display:block;width:1px;height:1px;min-width:100%}.tabulator .tabulator-footer .tabulator-paginator .tabulator-page:first-child{border-bottom-left-radius:3px;border-top-left-radius:3px}.tabulator .tabulator-footer .tabulator-paginator .tabulator-page:last-child{border-bottom-right-radius:3px;border-top-right-radius:3px}.tabulator-row:first-child>.tabulator-cell{border-top:none}.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding:0px}@keyframes highlight{0%{background:#ff9}100%{background:none}}.highlight-bg{animation:highlight 2s}.tabulator .tabulator-alert{background:none}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg{border:none}.tabulator .tabulator-alert .tabulator-alert-msg{background:initial;font-size:18px}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error{border:none}.bootgrid-placeholder{font-size:15px;text-align:center;white-space:normal;font-weight:400;margin:10px}.bootgrid-overlay{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,.5);display:flex;justify-content:center;align-items:center;z-index:99}.overlay>i{font-size:20px;color:#000}.tabulator{width:100%;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;font-size:13px;background:#fff;border:1px solid #eee;border-radius:0}.tabulator .tabulator-header{background:#fff;border-bottom:1px solid #eee;border-top:0;border-left:0;border-right:0}.tabulator .tabulator-header .tabulator-col{background:rgba(0,0,0,0);color:#333;border-right:1px solid #eee}.tabulator .tabulator-header .tabulator-col:last-child{border-right:0}.tabulator .tabulator-header .tabulator-col .tabulator-col-content{padding:10px 0 10px 20px;line-height:1.428571429}.tabulator .tabulator-header .tabulator-col.tabulator-sortable:hover{background:#f5f5f5}.tabulator .tabulator-tableholder{background:#fff}.tabulator-row{background:#fff;color:#333;border-top:1px solid #eee}.tabulator-row .tabulator-cell{padding:10px 0 10px 20px;line-height:1.428571429;vertical-align:top;border-right:1px solid #eee;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left,.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left,.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right,.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-right{border-color:#eee;border-width:1px}.tabulator-row .tabulator-cell:last-child{border-right:0}.tabulator-header .tabulator-row-header input,.tabulator-header .tabulator-cell input{margin-left:-24px}.tabulator-table .tabulator-row-header input,.tabulator-table .tabulator-cell input{margin-left:-18px}.tabulator.tabulator-striped .tabulator-row:nth-child(odd){background:#fbfbfb}.tabulator.tabulator-hover .tabulator-row:hover{background:#f5f5f5 !important}.tabulator-row.tabulator-selected{background:#f5f5f5 !important}.tabulator-row.tabulator-selected:hover{background:#f5f5f5 !important}.tabulator .tabulator-footer{background:#fff;border-top:1px solid #eee;color:#555}.tabulator .tabulator-footer .tabulator-page{border:1px solid #eee;background:#fff;color:#333;border-radius:0;margin:0;padding:6px 12px;color:#fff;background-color:#51aded;border-color:#51aded}.tabulator .tabulator-footer .tabulator-page:hover,.tabulator .tabulator-footer .tabulator-page:focus,.tabulator .tabulator-footer .tabulator-page:active,.tabulator .tabulator-footer .tabulator-page.active,.open>.tabulator .tabulator-footer .tabulator-page.dropdown-toggle,.tabulator .tabulator-footer .tabulator-page :not(disabled):hover{color:#fff !important;background-color:#2397e8 !important;border-color:#1a93e7 !important}.tabulator .tabulator-footer .tabulator-page:active,.tabulator .tabulator-footer .tabulator-page.active,.open>.tabulator .tabulator-footer .tabulator-page.dropdown-toggle{background-image:none}.tabulator .tabulator-footer .tabulator-page.disabled,.tabulator .tabulator-footer .tabulator-page.disabled:hover,.tabulator .tabulator-footer .tabulator-page.disabled:focus,.tabulator .tabulator-footer .tabulator-page.disabled:active,.tabulator .tabulator-footer .tabulator-page.disabled.active,.tabulator .tabulator-footer .tabulator-page[disabled],.tabulator .tabulator-footer .tabulator-page[disabled]:hover,.tabulator .tabulator-footer .tabulator-page[disabled]:focus,.tabulator .tabulator-footer .tabulator-page[disabled]:active,.tabulator .tabulator-footer .tabulator-page[disabled].active,fieldset[disabled] .tabulator .tabulator-footer .tabulator-page,fieldset[disabled] .tabulator .tabulator-footer .tabulator-page:hover,fieldset[disabled] .tabulator .tabulator-footer .tabulator-page:focus,fieldset[disabled] .tabulator .tabulator-footer .tabulator-page:active,fieldset[disabled] .tabulator .tabulator-footer .tabulator-page.active{background-color:#51aded;border-color:#51aded}.tabulator .tabulator-footer .tabulator-page .badge{color:#51aded;background-color:#fff}.tabulator-row.tabulator-row-even{background-color:rgba(0,0,0,0)}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title{overflow:visible}.tabulator-row.tabulator-selected{background-color:#9abcea}.tabulator .tabulator-alert{align-items:center;background:rgba(255,255,255,.5);display:flex;height:100%;left:0;position:absolute;text-align:center;top:0;width:100%;z-index:100}.bootgrid-header .actionBar .btn-group>.btn-group .dropdown-menu .dropdown-item.dropdown-item-checkbox,.bootgrid-footer .infoBar .btn-group>.btn-group .dropdown-menu .dropdown-item.dropdown-item-checkbox,.bootgrid-header .actionBar .btn-group>.btn-group .dropdown-menu .dropdown-item .dropdown-item-checkbox,.bootgrid-footer .infoBar .btn-group>.btn-group .dropdown-menu .dropdown-item .dropdown-item-checkbox{margin:0 8px 4px -8px !important}/*# sourceMappingURL=opnsense-bootgrid.css.map */ diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/default-logo.svg b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/default-logo.svg index 1fc57d76c..688ba464e 100644 --- a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/default-logo.svg +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/default-logo.svg @@ -1,95 +1,41 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/favicon.png b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/favicon.png index 3e6dde972..9a8472bc4 100644 Binary files a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/favicon.png and b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/favicon.png differ diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/icon-logo.svg b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/icon-logo.svg index 2e3ac8998..702d2a847 100644 --- a/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/icon-logo.svg +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/build/images/icon-logo.svg @@ -1,84 +1,10 @@ - - - + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/misc/theme-cicada/Makefile b/misc/theme-cicada/Makefile index 783548375..e7d7ac36e 100644 --- a/misc/theme-cicada/Makefile +++ b/misc/theme-cicada/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= theme-cicada -PLUGIN_VERSION= 1.38 +PLUGIN_VERSION= 1.40 PLUGIN_COMMENT= The cicada theme - dark grey onyx PLUGIN_MAINTAINER= rene@team-rebellion.net PLUGIN_NO_ABI= yes diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/assets/stylesheets/dashboard.scss b/misc/theme-cicada/src/opnsense/www/themes/cicada/assets/stylesheets/dashboard.scss index 2eb8ea4d3..88e697987 100644 --- a/misc/theme-cicada/src/opnsense/www/themes/cicada/assets/stylesheets/dashboard.scss +++ b/misc/theme-cicada/src/opnsense/www/themes/cicada/assets/stylesheets/dashboard.scss @@ -84,10 +84,20 @@ td { margin: 5px; } +.canvas-container-noaspectratio { + position: relative; +} + .canvas-container { position: relative; } +.canvas-container > canvas { + /* ChartJS v4 workaround: https://github.com/chartjs/Chart.js/issues/11005 */ + width: 100% !important; + height: 100% !important; +} + .cpu-canvas-container { display: flex; flex-direction: column; diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/assets/stylesheets/main.scss b/misc/theme-cicada/src/opnsense/www/themes/cicada/assets/stylesheets/main.scss index a7e374d6b..423497796 100644 --- a/misc/theme-cicada/src/opnsense/www/themes/cicada/assets/stylesheets/main.scss +++ b/misc/theme-cicada/src/opnsense/www/themes/cicada/assets/stylesheets/main.scss @@ -1453,7 +1453,7 @@ h6 { } h1, .h1 { - font-size: 22px; + font-size: 21px; text-transform: uppercase; } @@ -5363,7 +5363,7 @@ tbody.collapse.in { &.pull-right { right: 0; left: auto; - background-color: #e5e5e5; + background-color: #222; } .divider { @@ -6242,6 +6242,8 @@ a:focus { } .navbar-header { + padding-top: 12px; + &:before { content: " "; display: table; @@ -6384,22 +6386,9 @@ a:focus { border-width: 1px 0 0; } -.navbar-brand { - float: left; - padding: 15px 20px; - font-size: 18px; - height: 50px; - - &:hover, &:focus { - text-decoration: none; - } -} - -@media (min-width: 768px) { - .navbar > { - .container .navbar-brand, .container-fluid .navbar-brand { - margin-left: -20px; - } +.navbar-brand img { + &.brand-logo, &.brand-icon { + height: 30px; } } @@ -6901,7 +6890,7 @@ fieldset[disabled] .navbar-inverse .btn-link { line-height: 1.428571429; text-decoration: none; color: #bac3ca; - background-color: #505050; + background-color: #282828; border: 1px solid #191919; margin-left: -1px; cursor: pointer; @@ -6978,28 +6967,28 @@ fieldset[disabled] .navbar-inverse .btn-link { .disabled > { span { - color: #000; - background-color: #757575; + color: #4f4f4f; + background-color: #282828; border-color: #191919; cursor: not-allowed; &:hover, &:focus { - color: #000; - background-color: #757575; + color: #4f4f4f; + background-color: #282828; border-color: #191919; cursor: not-allowed; } } a { - color: #000; - background-color: #757575; + color: #4f4f4f; + background-color: #282828; border-color: #191919; cursor: not-allowed; &:hover, &:focus { - color: #000; - background-color: #757575; + color: #4f4f4f; + background-color: #282828; border-color: #191919; cursor: not-allowed; } @@ -7903,7 +7892,7 @@ a.list-group-item-danger { border-bottom: 1px solid #191919; border-top-right-radius: 2px; border-top-left-radius: 2px; - background-color: #2D2D2D !important; + background-color: #202020 !important; > .dropdown .dropdown-toggle { color: inherit; @@ -7923,8 +7912,8 @@ a.list-group-item-danger { .panel-footer { padding: 10px 15px; - background-color: #f5f5f5; - border-top: 1px solid #ddd; + background-color: #202020; + border-top: 1px solid #191919; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; } @@ -9161,7 +9150,7 @@ button.close { .show { display: block !important; - color: #FFF; + color: #3c8014; } .invisible { @@ -9758,7 +9747,7 @@ main.page-content.col-lg-12 { } a.list-group-item.active-menu-title { - color: #bac3ca !important; + color: #dd630d !important; background-color: #151515 !important; } } @@ -9906,6 +9895,7 @@ button.toggle-sidebar { margin-top: 18px; float: left; outline: none; + padding-left: 20px; } /* COLLAPSE SIDEBAR END*/ @@ -10138,10 +10128,6 @@ button.toggle-sidebar { border-radius: 0 !important; } -.navbar-brand { - padding: 10px 20px; -} - .label-opnsense { /* emulates btn */ display: inline-block; diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/dashboard.css b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/dashboard.css index 4725d9e07..7d784bb26 100644 --- a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/dashboard.css +++ b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/dashboard.css @@ -79,10 +79,20 @@ td { margin: 5px; } +.canvas-container-noaspectratio { + position: relative; +} + .canvas-container { position: relative; } +.canvas-container > canvas { + /* ChartJS v4 workaround: https://github.com/chartjs/Chart.js/issues/11005 */ + width: 100% !important; + height: 100% !important; +} + .cpu-canvas-container { display: flex; flex-direction: column; diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/jquery.bootgrid.css b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/jquery.bootgrid.css index c455999db..9f4c326e3 100644 --- a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/jquery.bootgrid.css +++ b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/jquery.bootgrid.css @@ -18,12 +18,14 @@ vertical-align: middle; width: 180px; } -.bootgrid-header .search .glyphicon, -.bootgrid-footer .search .glyphicon { +.bootgrid-header .search .fa-solid, +.bootgrid-footer .search .fa-solid { top: 0; } .bootgrid-header .search .fa, -.bootgrid-footer .search .fa { +.bootgrid-header .search .fa-solid, +.bootgrid-footer .search .fa, +.bootgrid-footer .search .fa-solid { display: table-cell; } .bootgrid-header .search.search-field::-ms-clear, @@ -42,9 +44,6 @@ } .bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu, .bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu { - color: #fff; - background-color: #2a2a2a; - border-color: #1d1d1d; text-align: left; } .bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item, @@ -59,9 +58,9 @@ .bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item:hover, .bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item:focus, .bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item:focus { - color: #fff; + color: #FFF; text-decoration: none; - background-color: #ec6d12; + background-color: #dd630d; } .bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item.dropdown-item-checkbox, .bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item.dropdown-item-checkbox, @@ -81,7 +80,7 @@ outline: 0; } .bootgrid-table th > .column-header-anchor { - color: #fff; + color: #e8f1f9; cursor: not-allowed; display: block; position: relative; @@ -100,18 +99,14 @@ white-space: nowrap; } .bootgrid-table th > .column-header-anchor > .icon { - color: #fff; display: block; position: absolute; right: 0; top: 2px; } -.bootgrid-table th > .column-header-anchor > .glyphicon { - color: #fff; -} .bootgrid-table th:hover, .bootgrid-table th:active { - background: #4b4b4b; + background: #fafafa; } .bootgrid-table td { overflow: hidden; @@ -122,7 +117,7 @@ } .bootgrid-table td.loading, .bootgrid-table td.no-results { - background: none; + background: #202020; text-align: center; } .bootgrid-table th.select-cell, diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/main.css b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/main.css index a932ab707..d097eb55e 100644 --- a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/main.css +++ b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/main.css @@ -1050,7 +1050,7 @@ h6, .h6 { font-size: 75%; } h1, .h1 { - font-size: 22px; + font-size: 21px; text-transform: uppercase; } h2, .h2 { @@ -3192,7 +3192,7 @@ tbody.collapse.in { .dropdown-menu.pull-right { right: 0; left: auto; - background-color: #e5e5e5;} + background-color: #222;} .dropdown-menu .divider { height: 1px; @@ -3717,6 +3717,10 @@ tbody.collapse.in { .navbar { border-radius: 0; } } +.navbar-header { + padding-top: 12px; +} + .navbar-header:before, .navbar-header:after { content: " "; display: table; } @@ -3806,18 +3810,12 @@ tbody.collapse.in { margin-bottom: 0; border-width: 1px 0 0; } -.navbar-brand { - float: left; - padding: 15px 20px; - font-size: 18px; - height: 50px; } - .navbar-brand:hover, .navbar-brand:focus { - text-decoration: none; } - - @media (min-width: 768px) { - .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand { - margin-left: -20px; } } - +.navbar-brand img.brand-logo { + height: 30px; +} +.navbar-brand img.brand-icon { + height: 30px; +} .navbar-toggle { position: relative; float: left; @@ -4087,7 +4085,7 @@ tbody.collapse.in { line-height: 1.428571429; text-decoration: none; color: #bac3ca; - background-color: #505050; + background-color: #282828; border: 1px solid #191919; margin-left: -1px; cursor: pointer; } @@ -4124,8 +4122,8 @@ tbody.collapse.in { .pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:focus { - color: #000; - background-color: #757575; + color: #4f4f4f; + background-color: #282828; border-color: #191919; cursor: not-allowed; } @@ -4703,7 +4701,7 @@ a.list-group-item-danger { border-bottom: 1px solid #191919; border-top-right-radius: 2px; border-top-left-radius: 2px; - background-color: #2D2D2D !important; } + background-color: #202020 !important; } .panel-heading > .dropdown .dropdown-toggle { color: inherit; } @@ -4718,8 +4716,8 @@ a.list-group-item-danger { .panel-footer { padding: 10px 15px; - background-color: #f5f5f5; - border-top: 1px solid #ddd; + background-color: #202020; + border-top: 1px solid #191919; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; } @@ -5510,7 +5508,7 @@ button.close { .show { display: block !important; - color: #FFF; } + color: #3c8014; } .invisible { visibility: hidden; } @@ -6058,6 +6056,7 @@ button.toggle-sidebar { margin-top: 18px; float:left; outline:none; + padding-left: 20px; } /* COLLAPSE SIDEBAR END*/ @@ -6220,9 +6219,6 @@ button.toggle-sidebar { .nav-tabs-justified > li > a, .nav-tabs.nav-justified > li > a, .nav-tabs.nav-justified > li > a { border-radius: 0 !important; } -.navbar-brand { - padding: 10px 20px; } - .label-opnsense { /* emulates btn */ display: inline-block; diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/opnsense-bootgrid.css b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/opnsense-bootgrid.css new file mode 100644 index 000000000..9fe95901a --- /dev/null +++ b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/opnsense-bootgrid.css @@ -0,0 +1,299 @@ +/** +* Main theming elements +*/ +.tabulator { + font-size: 15px; + background-color: transparent; + border: none; + border-top: 1px solid #191919; +} + +/* theme: header */ +.tabulator .tabulator-header { + background-color: transparent; + border-bottom: none; + border-left: 1px solid #191919; + +} + +.tabulator .tabulator-header .tabulator-col { + border-right: 1px solid #191919; + background-color: #242424; + color: #bac3ca; +} + +.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover { + background-color: #151515; +} + +.tabulator .tabulator-headers { + height: 100% !important; /* make sure the border below appears */ + border-bottom: 1px solid #191919; +} + +/*------------*/ +/* theme: body */ +.tabulator .tabulator-tableholder .tabulator-table { + background-color: transparent; + border-left: 1px solid #191919; +} + +.tabulator-row { + background-color: transparent; + color: #bac3ca; +} + +.tabulator-row.tabulator-row-odd { + background-color: #242424; +} + +.tabulator-row.tabulator-row-odd.tabulator-selectable:hover:not(.tabulator-selected) { + background-color: #242424; +} + +.tabulator-row.tabulator-row-even { + background-color: #242424; +} + +.tabulator-row.tabulator-row-even.tabulator-selectable:hover:not(.tabulator-selected) { + background-color: #242424; +} + +.tabulator .tabulator-tableholder { + background-color: transparent; +} + +.tabulator-row.tabulator-selected { + background-color: #242424; +} + +.tabulator-row.tabulator-selected:hover { + cursor: pointer; + background-color: #242424; +} + +.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg { + color: #373736; /* spinner icon */ +} + +/* Command column styles */ +.tabulator-col.tabulator-frozen.tabulator-frozen-right { +} + +.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right { +} + +/* Checkbox column styles */ +.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left { + border-right: 1px solid #191919; /* separates checkbox column from table rows */ + border-left: 1px solid #191919; +} + +.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left { + border-right: 1px solid #191919; + border-top: 1px solid #191919; + border-bottom: 1px solid #191919; + border-left: 1px solid #191919; +} + +.tabulator-col.tabulator-row-header.tabulator-frozen.tabulator-frozen-left { +} + +.tabulator-cell.tabulator-row-header.tabulator-frozen.tabulator-frozen-left { +} + +/*------------*/ +/* theme: cells */ +.tabulator-row .tabulator-cell { + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.2; + vertical-align: top; + border-bottom: 1px solid #191919; + border-right: 1px solid #191919; +} + +/*-------------*/ +/* theme: footer */ +.tabulator .tabulator-footer { + border-top: none; + background-color: transparent; +} + +.tabulator .tabulator-footer .tabulator-page { + background-color: #262626; + color: #bac3ca; +} + +.tabulator .tabulator-footer .tabulator-page.active { + background-color: #dd630d; + border-color: #191919; + cursor: default; + color: #bac3ca; +} + +.tabulator .tabulator-footer .tabulator-page:not(disabled):hover { + color: #bac3ca; + background-color: #dd630d; + border-color: #191919; +} + +.tabulator .tabulator-footer .tabulator-page:not(disabled).active:hover { + background-color: #dd630d; + border-color: #191919; + color: #bac3ca; + cursor: default; +} + +.tabulator-page-counter { + color: #bac3ca; +} + +.tabulator .tabulator-footer .tabulator-paginator { + color: #bac3ca; +} + +.tabulator .tabulator-footer .tabulator-page { + border-radius: 0px; + margin: 0px; + padding: 6px 12px; + border: 1px solid #191919; +} + +/*----------*/ +/* end of main theming elements */ +.tabulator-paginator { + flex: 0 !important; +} + +.tabulator-page-counter { + flex: 1; + text-align: right; +} + +.tabulator-col-sorter i { + display: flex; + justify-content: center; + align-items: center; +} + +.tabulator .tabulator-header .tabulator-col { + padding: 5px; +} + +.tabulator .tabulator-headers > :first-child { + padding-left: 10px; +} + +.opnsense-bootgrid-responsive { + white-space: wrap !important; + text-overflow: inherit !important; + overflow: visible !important; + word-break: break-word !important; +} + +.opnsense-bootgrid-ellipsis { + overflow: hidden !important; + white-space: nowrap !important; + text-overflow: ellipsis !important; +} + +.tabulator-row > :first-child { + padding-left: 10px; +} + +.tabulator .tabulator-header .tabulator-col .tabulator-col-content { + padding: 0px; +} + +.tabulator-page-counter { + padding-right: 20px; +} + +.bootgrid-footer-commands { + width: 90px; + padding-left: 8px; +} + +.tabulator-tableholder::after { + content: ""; + display: block; + width: 1px; + height: 1px; + min-width: 100%; +} + +.tabulator .tabulator-footer .tabulator-paginator .tabulator-page:first-child { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; +} + +.tabulator .tabulator-footer .tabulator-paginator .tabulator-page:last-child { + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; +} + +/* border line corrections and hover/selection behavior */ +.tabulator-row:first-child > .tabulator-cell { + border-top: none; +} + +.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title { + padding: 0px; +} + +/* Row highlight behavior */ +@keyframes highlight { + 0% { + background: #242424; + } + 100% { + background: none; + } +} +.highlight-bg { + animation: highlight 2s; +} + +/* Tabulator "loading" and "error" overrides */ +.tabulator .tabulator-alert { + background: none; +} + +.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg { + border: none; +} + +.tabulator .tabulator-alert .tabulator-alert-msg { + background: initial; + font-size: 18px; +} + +.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error { + border: none; +} + +.bootgrid-placeholder { + font-size: 15px; + text-align: center; + white-space: normal; + font-weight: 400; + margin: 10px; +} + +.bootgrid-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #242424; + display: flex; + justify-content: center; + align-items: center; + z-index: 99; +} +.overlay > i { + font-size: 20px; + color: black; +} diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/tabulator.min.css b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/tabulator.min.css new file mode 100644 index 000000000..7034e9655 --- /dev/null +++ b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/css/tabulator.min.css @@ -0,0 +1,2 @@ +.tabulator{background-color:#888;border:1px solid #191919;font-size:14px;overflow:hidden;position:relative;text-align:left;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.tabulator[tabulator-layout=fitDataFill] .tabulator-tableholder .tabulator-table{min-width:100%}.tabulator[tabulator-layout=fitDataTable]{display:inline-block}.tabulator.tabulator-block-select,.tabulator.tabulator-ranges .tabulator-cell:not(.tabulator-editing){user-select:none; }.tabulator .tabulator-header{background-color:#e6e6e6;border-bottom:1px solid #999;box-sizing:border-box;color:#555;font-weight:700;outline:none;overflow:hidden;position:relative;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;white-space:nowrap;width:100%}.tabulator .tabulator-header.tabulator-header-hidden{display:none}.tabulator .tabulator-header .tabulator-header-contents{overflow:hidden;position:relative}.tabulator .tabulator-header .tabulator-header-contents .tabulator-headers{display:inline-block}.tabulator .tabulator-header .tabulator-col{background:#e6e6e6;border-right:1px solid #aaa;box-sizing:border-box;display:inline-flex;flex-direction:column;justify-content:flex-start;overflow:hidden;position:relative;text-align:left;vertical-align:bottom}.tabulator .tabulator-header .tabulator-col.tabulator-moving{background:#cdcdcd;border:1px solid #999;pointer-events:none;position:absolute}.tabulator .tabulator-header .tabulator-col.tabulator-range-highlight{background-color:#d6d6d6;color:#000}.tabulator .tabulator-header .tabulator-col.tabulator-range-selected{background-color:#3876ca;color:#fff}.tabulator .tabulator-header .tabulator-col .tabulator-col-content{box-sizing:border-box;padding:4px;position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button{padding:0 8px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button:hover{cursor:pointer;opacity:.6}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title-holder{position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title{box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap;width:100%}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title.tabulator-col-title-wrap{text-overflow:clip;white-space:normal}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-title-editor{background:#fff;border:1px solid #999;box-sizing:border-box;padding:1px;width:100%}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-header-popup-button+.tabulator-title-editor{width:calc(100% - 22px)}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{align-items:center;bottom:0;display:flex;position:absolute;right:4px;top:0}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:6px solid #bbb;border-left:6px solid transparent;border-right:6px solid transparent;height:0;width:0}.tabulator .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{border-top:1px solid #aaa;display:flex;margin-right:-1px;overflow:hidden;position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter{box-sizing:border-box;margin-top:2px;position:relative;text-align:center;width:100%}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter textarea{height:auto!important}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter svg{margin-top:3px}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input::-ms-clear{height:0;width:0}.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-right:25px}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover{background-color:#cdcdcd;cursor:pointer}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter{color:#bbb}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{border-bottom:6px solid #555;cursor:pointer}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:6px solid #bbb;border-top:none}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{border-bottom:6px solid #555;cursor:pointer}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:6px solid #666;border-top:none}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{border-top:6px solid #555;cursor:pointer}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:none;border-top:6px solid #666;color:#666}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical .tabulator-col-content .tabulator-col-title{align-items:center;display:flex;justify-content:center;text-orientation:mixed;writing-mode:vertical-rl}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-col-vertical-flip .tabulator-col-title{transform:rotate(180deg)}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-title{padding-right:0;padding-top:20px}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable.tabulator-col-vertical-flip .tabulator-col-title{padding-bottom:20px;padding-right:0}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-sorter{bottom:auto;justify-content:center;left:0;right:0;top:4px}.tabulator .tabulator-header .tabulator-frozen{left:0;position:sticky;z-index:11}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left{border-right:2px solid #151515}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-right{border-left:2px solid #151515}.tabulator .tabulator-header .tabulator-calcs-holder{background:#f3f3f3!important;border-bottom:1px solid #aaa;border-top:1px solid #aaa;box-sizing:border-box;display:inline-block}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row{background:#f3f3f3!important}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle{display:none}.tabulator .tabulator-header .tabulator-frozen-rows-holder{display:inline-block}.tabulator .tabulator-header .tabulator-frozen-rows-holder:empty{display:none}.tabulator .tabulator-tableholder{-webkit-overflow-scrolling:touch;overflow:auto;position:relative;white-space:nowrap;width:100%}.tabulator .tabulator-tableholder:focus{outline:none}.tabulator .tabulator-tableholder .tabulator-placeholder{align-items:center;box-sizing:border-box;display:flex;justify-content:center;min-width:100%;width:100%}.tabulator .tabulator-tableholder .tabulator-placeholder[tabulator-render-mode=virtual]{min-height:100%}.tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-placeholder-contents{color:#ccc;display:inline-block;font-size:20px;font-weight:700;padding:10px;text-align:center;white-space:normal}.tabulator .tabulator-tableholder .tabulator-table{background-color:#fff;color:#333;display:inline-block;overflow:visible;position:relative;white-space:nowrap}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs{background:#e2e2e2!important;font-weight:700}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-top{border-bottom:2px solid #aaa}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-bottom{border-top:2px solid #aaa}.tabulator .tabulator-tableholder .tabulator-range-overlay{inset:0;pointer-events:none;position:absolute;z-index:10}.tabulator .tabulator-tableholder .tabulator-range-overlay .tabulator-range{border:1px solid #2975dd;box-sizing:border-box;position:absolute}.tabulator .tabulator-tableholder .tabulator-range-overlay .tabulator-range.tabulator-range-active:after{background-color:#2975dd;border-radius:999px;bottom:-3px;content:"";height:6px;position:absolute;right:-3px;width:6px}.tabulator .tabulator-tableholder .tabulator-range-overlay .tabulator-range-cell-active{border:2px solid #2975dd;box-sizing:border-box;position:absolute}.tabulator .tabulator-footer{background-color:#e6e6e6;border-top:1px solid #999;color:#555;font-weight:700;user-select:none;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;white-space:nowrap}.tabulator .tabulator-footer .tabulator-footer-contents{align-items:center;display:flex;flex-direction:row;justify-content:space-between;padding:5px 10px}.tabulator .tabulator-footer .tabulator-footer-contents:empty{display:none}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs{margin-top:-5px;overflow-x:auto}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab{border:1px solid #999;border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-top:none;display:inline-block;font-size:.9em;padding:5px}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab:hover{cursor:pointer;opacity:.7}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab.tabulator-spreadsheet-tab-active{background:#fff}.tabulator .tabulator-footer .tabulator-calcs-holder{background:#f3f3f3!important;border-bottom:1px solid #aaa;border-top:1px solid #aaa;box-sizing:border-box;overflow:hidden;text-align:left;width:100%}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{background:#f3f3f3!important;display:inline-block}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle{display:none}.tabulator .tabulator-footer .tabulator-calcs-holder:only-child{border-bottom:none;margin-bottom:-5px}.tabulator .tabulator-footer>*+.tabulator-page-counter{margin-left:10px}.tabulator .tabulator-footer .tabulator-page-counter{font-weight:400}.tabulator .tabulator-footer .tabulator-paginator{color:#555;flex:1;font-family:inherit;font-size:inherit;font-weight:inherit;text-align:right}.tabulator .tabulator-footer .tabulator-page-size{border:1px solid #aaa;border-radius:3px;display:inline-block;margin:0 5px;padding:2px 5px}.tabulator .tabulator-footer .tabulator-pages{margin:0 7px}.tabulator .tabulator-footer .tabulator-page{background:hsla(0,0%,100%,.2);border:1px solid #aaa;border-radius:3px;display:inline-block;margin:0 2px;padding:2px 5px}.tabulator .tabulator-footer .tabulator-page.active{color:#d00}.tabulator .tabulator-footer .tabulator-page:disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-footer .tabulator-page:not(disabled):hover{background:rgba(0,0,0,.2);color:#fff;cursor:pointer}}.tabulator .tabulator-col-resize-handle{display:inline-block;margin-left:-3px;margin-right:-3px;position:relative;vertical-align:middle;width:6px;z-index:11}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-col-resize-handle:hover{cursor:ew-resize}}.tabulator .tabulator-col-resize-handle:last-of-type{margin-right:0;width:3px}.tabulator .tabulator-col-resize-guide{background-color:#999;height:100%;margin-left:-.5px;opacity:.5;position:absolute;top:0;width:4px}.tabulator .tabulator-row-resize-guide{background-color:#999;height:4px;left:0;margin-top:-.5px;opacity:.5;position:absolute;width:100%}.tabulator .tabulator-alert{align-items:center;background:rgba(0,0,0,.4);display:flex;height:100%;left:0;position:absolute;text-align:center;top:0;width:100%;z-index:100}.tabulator .tabulator-alert .tabulator-alert-msg{background:#fff;border-radius:10px;display:inline-block;font-size:16px;font-weight:700;margin:0 auto;padding:10px 20px}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg{border:4px solid #333;color:#000}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error{border:4px solid #d00;color:#590000}.tabulator-row{background-color:#fff;box-sizing:border-box;min-height:22px;position:relative}.tabulator-row.tabulator-row-even{background-color:#efefef}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selectable:hover{background-color:#bbb;cursor:pointer}}.tabulator-row.tabulator-selected{background-color:#9abcea}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selected:hover{background-color:#769bcc;cursor:pointer}}.tabulator-row.tabulator-row-moving{background:#fff;border:1px solid #000}.tabulator-row.tabulator-moving{border-bottom:1px solid #aaa;border-top:1px solid #aaa;pointer-events:none;position:absolute;z-index:15}.tabulator-row.tabulator-range-highlight .tabulator-cell.tabulator-range-row-header{background-color:#d6d6d6;color:#000}.tabulator-row.tabulator-range-highlight.tabulator-range-selected .tabulator-cell.tabulator-range-row-header,.tabulator-row.tabulator-range-selected .tabulator-cell.tabulator-range-row-header{background-color:#3876ca;color:#fff}.tabulator-row .tabulator-row-resize-handle{bottom:0;height:5px;left:0;position:absolute;right:0}.tabulator-row .tabulator-row-resize-handle.prev{bottom:auto;top:0}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-row-resize-handle:hover{cursor:ns-resize}}.tabulator-row .tabulator-responsive-collapse{border-bottom:1px solid #aaa;border-top:1px solid #aaa;box-sizing:border-box;padding:5px}.tabulator-row .tabulator-responsive-collapse:empty{display:none}.tabulator-row .tabulator-responsive-collapse table{font-size:14px}.tabulator-row .tabulator-responsive-collapse table tr td{position:relative}.tabulator-row .tabulator-responsive-collapse table tr td:first-of-type{padding-right:10px}.tabulator-row .tabulator-cell{border-right:1px solid #aaa;box-sizing:border-box;display:inline-block;outline:none;overflow:hidden;padding:4px;position:relative;text-overflow:ellipsis;vertical-align:middle;white-space:nowrap}.tabulator-row .tabulator-cell.tabulator-row-header{background:#e6e6e6;border-bottom:1px solid #aaa;border-right:1px solid #999}.tabulator-row .tabulator-cell.tabulator-frozen{background-color:inherit;display:inline-block;left:0;position:sticky;z-index:11}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-right:2px solid #151515}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-left:2px solid #151515}.tabulator-row .tabulator-cell.tabulator-editing{border:1px solid #1d68cd;outline:none;padding:0}.tabulator-row .tabulator-cell.tabulator-editing input,.tabulator-row .tabulator-cell.tabulator-editing select{background:transparent;border:1px;outline:none}.tabulator-row .tabulator-cell.tabulator-validation-fail{border:1px solid #d00}.tabulator-row .tabulator-cell.tabulator-validation-fail input,.tabulator-row .tabulator-cell.tabulator-validation-fail select{background:transparent;border:1px;color:#d00}.tabulator-row .tabulator-cell.tabulator-row-handle{align-items:center;display:inline-flex;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box{width:80%}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box .tabulator-row-handle-bar{background:#666;height:3px;margin-top:2px;width:100%}.tabulator-row .tabulator-cell.tabulator-range-selected:not(.tabulator-range-only-cell-selected):not(.tabulator-range-row-header){background-color:#9abcea}.tabulator-row .tabulator-cell .tabulator-data-tree-branch-empty{display:inline-block;width:7px}.tabulator-row .tabulator-cell .tabulator-data-tree-branch{border-bottom:2px solid #aaa;border-bottom-left-radius:1px;border-left:2px solid #aaa;display:inline-block;height:9px;margin-right:5px;margin-top:-9px;vertical-align:middle;width:7px}.tabulator-row .tabulator-cell .tabulator-data-tree-control{align-items:center;background:rgba(0,0,0,.1);border:1px solid #333;border-radius:2px;display:inline-flex;height:11px;justify-content:center;margin-right:5px;overflow:hidden;vertical-align:middle;width:11px}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-data-tree-control:hover{background:rgba(0,0,0,.2);cursor:pointer}}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse{background:transparent;display:inline-block;height:7px;position:relative;width:1px}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{background:#333;content:"";height:1px;left:-3px;position:absolute;top:3px;width:7px}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand{background:#333;display:inline-block;height:7px;position:relative;width:1px}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{background:#333;content:"";height:1px;left:-3px;position:absolute;top:3px;width:7px}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle{align-items:center;background:#666;border-radius:20px;color:#fff;display:inline-flex;font-size:1.1em;font-weight:700;height:15px;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;width:15px}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle:hover{cursor:pointer;opacity:.7}}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-close{display:initial}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-open{display:none}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle svg{stroke:#fff}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle .tabulator-responsive-collapse-toggle-close{display:none}.tabulator-row .tabulator-cell .tabulator-traffic-light{border-radius:14px;display:inline-block;height:14px;width:14px}.tabulator-row.tabulator-group{background:#ccc;border-bottom:1px solid #999;border-right:1px solid #aaa;border-top:1px solid #999;box-sizing:border-box;font-weight:700;min-width:100%;padding:5px 5px 5px 10px}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-group:hover{background-color:rgba(0,0,0,.1);cursor:pointer}}.tabulator-row.tabulator-group.tabulator-group-visible .tabulator-arrow{border-bottom:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;margin-right:10px}.tabulator-row.tabulator-group.tabulator-group-level-1{padding-left:30px}.tabulator-row.tabulator-group.tabulator-group-level-2{padding-left:50px}.tabulator-row.tabulator-group.tabulator-group-level-3{padding-left:70px}.tabulator-row.tabulator-group.tabulator-group-level-4{padding-left:90px}.tabulator-row.tabulator-group.tabulator-group-level-5{padding-left:110px}.tabulator-row.tabulator-group .tabulator-group-toggle{display:inline-block}.tabulator-row.tabulator-group .tabulator-arrow{border-bottom:6px solid transparent;border-left:6px solid #666;border-right:0;border-top:6px solid transparent;display:inline-block;height:0;margin-right:16px;vertical-align:middle;width:0}.tabulator-row.tabulator-group span{color:#d00;margin-left:10px}.tabulator-toggle{background:#dcdcdc;border:1px solid #ccc;box-sizing:border-box;display:flex;flex-direction:row}.tabulator-toggle.tabulator-toggle-on{background:#1c6cc2}.tabulator-toggle .tabulator-toggle-switch{background:#fff;border:1px solid #ccc;box-sizing:border-box}.tabulator-popup-container{-webkit-overflow-scrolling:touch;background:#fff;border:1px solid #aaa;box-shadow:0 0 5px 0 rgba(0,0,0,.2);box-sizing:border-box;display:inline-block;font-size:14px;overflow-y:auto;position:absolute;z-index:10000}.tabulator-popup{border-radius:3px;padding:5px}.tabulator-tooltip{border-radius:2px;box-shadow:none;font-size:12px;max-width:Min(500px,100%);padding:3px 5px;pointer-events:none}.tabulator-menu .tabulator-menu-item{box-sizing:border-box;padding:5px 10px;position:relative;user-select:none}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator-menu .tabulator-menu-item:not(.tabulator-menu-item-disabled):hover{background:#efefef;cursor:pointer}}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu{padding-right:25px}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu:after{border-color:#aaa;border-style:solid;border-width:1px 1px 0 0;content:"";display:inline-block;height:7px;position:absolute;right:10px;top:calc(5px + .4em);transform:rotate(45deg);vertical-align:top;width:7px}.tabulator-menu .tabulator-menu-separator{border-top:1px solid #aaa}.tabulator-edit-list{-webkit-overflow-scrolling:touch;font-size:14px;max-height:200px;overflow-y:auto}.tabulator-edit-list .tabulator-edit-list-item{color:#333;outline:none;padding:4px}.tabulator-edit-list .tabulator-edit-list-item.active{background:#1d68cd;color:#fff}.tabulator-edit-list .tabulator-edit-list-item.active.focused{outline:1px solid hsla(0,0%,100%,.5)}.tabulator-edit-list .tabulator-edit-list-item.focused{outline:1px solid #1d68cd}@media (hover:hover) and (pointer:fine){.tabulator-edit-list .tabulator-edit-list-item:hover{background:#1d68cd;color:#fff;cursor:pointer}}.tabulator-edit-list .tabulator-edit-list-placeholder{color:#333;padding:4px;text-align:center}.tabulator-edit-list .tabulator-edit-list-group{border-bottom:1px solid #aaa;color:#333;font-weight:700;padding:6px 4px 4px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-2,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-2{padding-left:12px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-3,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-3{padding-left:20px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-4,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-4{padding-left:28px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-5,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-5{padding-left:36px}.tabulator.tabulator-ltr{direction:ltr}.tabulator.tabulator-rtl{direction:rtl;text-align:initial}.tabulator.tabulator-rtl .tabulator-header .tabulator-col{border-left:1px solid #aaa;border-right:initial;text-align:initial}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{margin-left:-1px;margin-right:0}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-left:25px;padding-right:0}.tabulator.tabulator-rtl .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{left:8px;right:auto}.tabulator.tabulator-rtl .tabulator-tableholder .tabulator-range-overlay .tabulator-range.tabulator-range-active:after{background-color:#2975dd;border-radius:999px;bottom:-3px;content:"";height:6px;left:-3px;position:absolute;right:auto;width:6px}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell{border-left:1px solid #aaa;border-right:initial}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-branch{border-bottom-left-radius:0;border-bottom-right-radius:1px;border-left:initial;border-right:2px solid #aaa;margin-left:5px;margin-right:0}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-control{margin-left:5px;margin-right:0}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-left:2px solid #aaa}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-right:2px solid #aaa}.tabulator.tabulator-rtl .tabulator-row .tabulator-col-resize-handle:last-of-type{margin-left:0;margin-right:-3px;width:3px}.tabulator.tabulator-rtl .tabulator-footer .tabulator-calcs-holder{text-align:initial}.tabulator-print-fullscreen{bottom:0;left:0;position:absolute;right:0;top:0;z-index:10000}body.tabulator-print-fullscreen-hide>:not(.tabulator-print-fullscreen){display:none!important}.tabulator-print-table{border-collapse:collapse}.tabulator-print-table .tabulator-data-tree-branch{border-bottom:2px solid #aaa;border-bottom-left-radius:1px;border-left:2px solid #aaa;display:inline-block;height:9px;margin-right:5px;margin-top:-9px;vertical-align:middle;width:7px}.tabulator-print-table .tabulator-print-table-group{background:#ccc;border-bottom:1px solid #999;border-right:1px solid #aaa;border-top:1px solid #999;box-sizing:border-box;font-weight:700;min-width:100%;padding:5px 5px 5px 10px}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-print-table-group:hover{background-color:rgba(0,0,0,.1);cursor:pointer}}.tabulator-print-table .tabulator-print-table-group.tabulator-group-visible .tabulator-arrow{border-bottom:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;margin-right:10px}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-1 td{padding-left:30px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-2 td{padding-left:50px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-3 td{padding-left:70px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-4 td{padding-left:90px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-5 td{padding-left:110px!important}.tabulator-print-table .tabulator-print-table-group .tabulator-group-toggle{display:inline-block}.tabulator-print-table .tabulator-print-table-group .tabulator-arrow{border-bottom:6px solid transparent;border-left:6px solid #666;border-right:0;border-top:6px solid transparent;display:inline-block;height:0;margin-right:16px;vertical-align:middle;width:0}.tabulator-print-table .tabulator-print-table-group span{color:#d00;margin-left:10px}.tabulator-print-table .tabulator-data-tree-control{align-items:center;background:rgba(0,0,0,.1);border:1px solid #333;border-radius:2px;display:inline-flex;height:11px;justify-content:center;margin-right:5px;overflow:hidden;vertical-align:middle;width:11px}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-data-tree-control:hover{background:rgba(0,0,0,.2);cursor:pointer}}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse{background:transparent;display:inline-block;height:7px;position:relative;width:1px}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{background:#333;content:"";height:1px;left:-3px;position:absolute;top:3px;width:7px}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand{background:#333;display:inline-block;height:7px;position:relative;width:1px}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{background:#333;content:"";height:1px;left:-3px;position:absolute;top:3px;width:7px} +/*# sourceMappingURL=tabulator.min.css.map */ \ No newline at end of file diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/default-logo.png b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/default-logo.png index 9f98843f0..35122b0fc 100644 Binary files a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/default-logo.png and b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/default-logo.png differ diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/favicon.png b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/favicon.png index 3e6dde972..9a8472bc4 100644 Binary files a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/favicon.png and b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/favicon.png differ diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/icon-logo.png b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/icon-logo.png deleted file mode 100644 index 31f8b2b24..000000000 Binary files a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/icon-logo.png and /dev/null differ diff --git a/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/icon-logo.svg b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/icon-logo.svg new file mode 100644 index 000000000..ce38f02f0 --- /dev/null +++ b/misc/theme-cicada/src/opnsense/www/themes/cicada/build/images/icon-logo.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/misc/theme-flexcolor/Makefile b/misc/theme-flexcolor/Makefile new file mode 100644 index 000000000..4ff210b34 --- /dev/null +++ b/misc/theme-flexcolor/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= theme-flexcolor +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= Theme with 3 different color schemes: black as default, light and dark-light +PLUGIN_MAINTAINER= iengels@web.de +PLUGIN_NO_ABI= yes + +.include "../../Mk/plugins.mk" diff --git a/misc/theme-flexcolor/pkg-descr b/misc/theme-flexcolor/pkg-descr new file mode 100644 index 000000000..484bd86da --- /dev/null +++ b/misc/theme-flexcolor/pkg-descr @@ -0,0 +1,5 @@ +Theme with the option to customize colors through color schemes. To change +the scheme, simply replace the corresponding default_scheme.css file in the +theme /build/css-folder with an alternative from the corresponding folders +in the /build/color_schemes/ folder. Feel free to adjust all colors by +changing the color codes in the scheme-file or to create your own schemes. diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/LICENSE b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/LICENSE new file mode 100644 index 000000000..4e7f5527b --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2014 bootstrap-select + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.eot b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.eot new file mode 100644 index 000000000..e9f423425 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.eot differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.otf b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.otf new file mode 100644 index 000000000..98dbee74d Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.otf differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.ttf b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.ttf new file mode 100644 index 000000000..5d65c9324 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.ttf differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.woff b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.woff new file mode 100644 index 000000000..d1d40f840 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.woff differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.eot b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.eot new file mode 100644 index 000000000..ba7f8d951 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.eot differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.otf b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.otf new file mode 100644 index 000000000..bdcfb27a4 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.otf differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.ttf b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.ttf new file mode 100644 index 000000000..44486cdc6 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.ttf differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.woff b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.woff new file mode 100644 index 000000000..460ab12a6 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Regular/SourceSansPro-Regular.woff differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.eot b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.eot new file mode 100644 index 000000000..2f7a52c45 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.eot differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.otf b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.otf new file mode 100644 index 000000000..fffdbafeb Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.otf differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.ttf b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.ttf new file mode 100644 index 000000000..86b00c067 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.ttf differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.woff b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.woff new file mode 100644 index 000000000..43379631b Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/SourceSansPro-Semibold/SourceSansPro-Semibold.woff differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot new file mode 100644 index 000000000..b93a4953f Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.svg b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.svg new file mode 100644 index 000000000..94fb5490a --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf new file mode 100644 index 000000000..1413fc609 Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.woff b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.woff new file mode 100644 index 000000000..9e612858f Binary files /dev/null and b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/fonts/bootstrap/glyphicons-halflings-regular.woff differ diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_alerts.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_alerts.scss new file mode 100644 index 000000000..e45de8305 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_alerts.scss @@ -0,0 +1,68 @@ +// +// Alerts +// -------------------------------------------------- + + +// Base styles +// ------------------------- + +.alert { + padding: $alert-padding; + margin-bottom: $line-height-computed; + border: 1px solid transparent; + border-radius: $alert-border-radius; + + // Headings for larger alerts + h4 { + margin-top: 0; + // Specified for the h4 to prevent conflicts of changing $headings-color + color: inherit; + } + // Provide class for links that match alerts + .alert-link { + font-weight: $alert-link-font-weight; + } + + // Improve alignment and spacing of inner content + > p, + > ul { + margin-bottom: 0; + } + > p + p { + margin-top: 5px; + } +} + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. +.alert-dismissible { + padding-right: ($alert-padding + 20); + + // Adjust close link position + .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; + } +} + +// Alternate styles +// +// Generate contextual modifier classes for colorizing the alert. + +.alert-success { + @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); +} +.alert-info { + @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); +} +.alert-warning { + @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); +} +.alert-danger { + @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_badges.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_badges.scss new file mode 100644 index 000000000..02394ae7f --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_badges.scss @@ -0,0 +1,57 @@ +// +// Badges +// -------------------------------------------------- + + +// Base class +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: $font-size-small; + font-weight: $badge-font-weight; + color: $badge-color; + line-height: $badge-line-height; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + background-color: $badge-bg; + border-radius: $badge-border-radius; + + // Empty badges collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for badges in buttons + .btn & { + position: relative; + top: -1px; + } + .btn-xs & { + top: 0; + padding: 1px 5px; + } + + // [converter] extracted a& to a.badge + + // Account for badges in navs + a.list-group-item.active > &, + .nav-pills > .active > a > & { + color: $badge-active-color; + background-color: $badge-active-bg; + } + .nav-pills > li > a > & { + margin-left: 3px; + } +} + +// Hover state, but only for links +a.badge { + &:hover, + &:focus { + color: $badge-link-hover-color; + text-decoration: none; + cursor: pointer; + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_breadcrumbs.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_breadcrumbs.scss new file mode 100644 index 000000000..3641e333b --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_breadcrumbs.scss @@ -0,0 +1,26 @@ +// +// Breadcrumbs +// -------------------------------------------------- + + +.breadcrumb { + padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal; + margin-bottom: $line-height-computed; + list-style: none; + background-color: $breadcrumb-bg; + border-radius: $border-radius-base; + + > li { + display: inline-block; + + + li:before { + content: "#{$breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space + padding: 0 5px; + color: $breadcrumb-color; + } + } + + > .active { + color: $breadcrumb-active-color; + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_button-groups.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_button-groups.scss new file mode 100644 index 000000000..63ccd927c --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_button-groups.scss @@ -0,0 +1,240 @@ +// +// Button groups +// -------------------------------------------------- + +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; // match .btn alignment given font-size hack above + > .btn { + position: relative; + float: left; + // Bring the "active" button to the front + &:hover, + &:focus, + &:active, + &.active { + z-index: 2; + } + &:focus { + // Remove focus outline when dropdown JS adds it after closing the menu + outline: 0; + } + } +} + +// Prevent double borders when buttons are next to each other +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: -1px; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + margin-left: -5px; // Offset the first child's margin + @include clearfix(); + + .btn-group, + .input-group { + float: left; + } + > .btn, + > .btn-group, + > .input-group { + margin-left: 5px; + } +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match +.btn-group > .btn:first-child { + margin-left: 0; + &:not(:last-child):not(.dropdown-toggle) { + @include border-right-radius(0); + } +} +// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + @include border-left-radius(0); +} + +// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child { + > .btn:last-child, + > .dropdown-toggle { + @include border-right-radius(0); + } +} +.btn-group > .btn-group:last-child > .btn:first-child { + @include border-left-radius(0); +} + +// On active and open, don't show outline +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-xs > .btn { @extend .btn-xs; } +.btn-group-sm > .btn { @extend .btn-sm; } +.btn-group-lg > .btn { @extend .btn-lg; } + + +// Split button dropdowns +// ---------------------- + +// Give the line between buttons some depth +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} + +// The clickable button for toggling the menu +// Remove the gradient and set the same inset shadow as the :active state +.btn-group.open .dropdown-toggle { + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + @include box-shadow(none); + } +} + + +// Reposition the caret +.btn .caret { + margin-left: 0; +} +// Carets in other button sizes +.btn-lg .caret { + border-width: $caret-width-large $caret-width-large 0; + border-bottom-width: 0; +} +// Upside down carets for .dropup +.dropup .btn-lg .caret { + border-width: 0 $caret-width-large $caret-width-large; +} + + +// Vertical button groups +// ---------------------- + +.btn-group-vertical { + > .btn, + > .btn-group, + > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; + } + + // Clear floats so dropdown menus can be properly placed + > .btn-group { + @include clearfix(); + > .btn { + float: none; + } + } + + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; + } +} + +.btn-group-vertical > .btn { + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + &:first-child:not(:last-child) { + border-top-right-radius: $border-radius-base; + @include border-bottom-radius(0); + } + &:last-child:not(:first-child) { + border-bottom-left-radius: $border-radius-base; + @include border-top-radius(0); + } +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + @include border-bottom-radius(0); + } +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + @include border-top-radius(0); +} + + + +// Justified button groups +// ---------------------- + +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; + > .btn, + > .btn-group { + float: none; + display: table-cell; + width: 1%; + } + > .btn-group .btn { + width: 100%; + } + + > .btn-group .dropdown-menu { + left: auto; + } +} + + +// Checkbox and radio options +// +// In order to support the browser's form validation feedback, powered by the +// `required` attribute, we have to "hide" the inputs via `opacity`. We cannot +// use `display: none;` or `visibility: hidden;` as that also hides the popover. +// This way, we ensure a DOM element is visible to position the popover from. +// +// See https://github.com/twbs/bootstrap/pull/12794 for more. + +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + position: absolute; + z-index: -1; + @include opacity(0); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_buttons.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_buttons.scss new file mode 100644 index 000000000..f2684c1b7 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_buttons.scss @@ -0,0 +1,159 @@ +// +// Buttons +// -------------------------------------------------- + + +// Base styles +// -------------------------------------------------- + +.btn { + display: inline-block; + margin-bottom: 0; // For input.btn + font-weight: $btn-font-weight; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + white-space: nowrap; + @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $border-radius-base); + @include user-select(none); + + @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); + + &, + &:active, + &.active { + &:focus { + @include tab-focus(); + } + } + + &:hover, + &:focus { + color: $btn-default-color; + text-decoration: none; + } + + &:active, + &.active { + outline: 0; + background-image: none; + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + cursor: not-allowed; + pointer-events: none; // Future-proof disabling of clicks + @include opacity(.65); + @include box-shadow(none); + } +} + + +// Alternate buttons +// -------------------------------------------------- + +.btn-default { + @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); +} +.btn-primary { + @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border); +} +// Success appears as green +.btn-success { + @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border); +} +// Info appears as blue-green +.btn-info { + @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border); +} +// Warning appears as orange +.btn-warning { + @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border); +} +// Danger and error appear as red +.btn-danger { + @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border); +} + + +// Link buttons +// ------------------------- + +// Make a button look and behave like a link +.btn-link { + color: $link-color; + font-weight: normal; + cursor: pointer; + border-radius: 0; + + &, + &:active, + &[disabled], + fieldset[disabled] & { + background-color: transparent; + @include box-shadow(none); + } + &, + &:hover, + &:focus, + &:active { + border-color: transparent; + } + &:hover, + &:focus { + color: $link-hover-color; + text-decoration: underline; + background-color: transparent; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: $btn-link-disabled-color; + text-decoration: none; + } + } +} + + +// Button Sizes +// -------------------------------------------------- + +.btn-lg { + // line-height: ensure even-numbered height of button next to large input + @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); +} +.btn-sm { + // line-height: ensure proper height of button next to small input + @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); +} +.btn-xs { + @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $border-radius-small); +} + + +// Block button +// -------------------------------------------------- + +.btn-block { + display: block; + width: 100%; +} + +// Vertically space out multiple block buttons +.btn-block + .btn-block { + margin-top: 5px; +} + +// Specificity overrides +input[type="submit"], +input[type="reset"], +input[type="button"] { + &.btn-block { + width: 100%; + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_carousel.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_carousel.scss new file mode 100644 index 000000000..e9e2f7ce1 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_carousel.scss @@ -0,0 +1,243 @@ +// +// Carousel +// -------------------------------------------------- + + +// Wrapper for the slide container and indicators +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + overflow: hidden; + width: 100%; + + > .item { + display: none; + position: relative; + @include transition(.6s ease-in-out left); + + // Account for jankitude on images + > img, + > a > img { + @include img-responsive(); + line-height: 1; + } + } + + > .active, + > .next, + > .prev { + display: block; + } + + > .active { + left: 0; + } + + > .next, + > .prev { + position: absolute; + top: 0; + width: 100%; + } + + > .next { + left: 100%; + } + > .prev { + left: -100%; + } + > .next.left, + > .prev.right { + left: 0; + } + + > .active.left { + left: -100%; + } + > .active.right { + left: 100%; + } + +} + +// Left/right controls for nav +// --------------------------- + +.carousel-control { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: $carousel-control-width; + @include opacity($carousel-control-opacity); + font-size: $carousel-control-font-size; + color: $carousel-control-color; + text-align: center; + text-shadow: $carousel-text-shadow; + // We can't have this transition here because WebKit cancels the carousel + // animation if you trip this while in the middle of another animation. + + // Set gradients for backgrounds + &.left { + @include gradient-horizontal($start-color: rgba(0,0,0,.5), $end-color: rgba(0,0,0,.0001)); + } + &.right { + left: auto; + right: 0; + @include gradient-horizontal($start-color: rgba(0,0,0,.0001), $end-color: rgba(0,0,0,.5)); + } + + // Hover/focus state + &:hover, + &:focus { + outline: 0; + color: $carousel-control-color; + text-decoration: none; + @include opacity(.9); + } + + // Toggles + .icon-prev, + .icon-next, + .glyphicon-chevron-left, + .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + } + .icon-prev, + .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; + } + .icon-next, + .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; + } + .icon-prev, + .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + font-family: serif; + } + + + .icon-prev { + &:before { + content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) + } + } + .icon-next { + &:before { + content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) + } + } +} + +// Optional indicator pips +// +// Add an unordered list with the following class and add a list item for each +// slide your carousel holds. + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; + list-style: none; + text-align: center; + + li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + border: 1px solid $carousel-indicator-border-color; + border-radius: 10px; + cursor: pointer; + + // IE8-9 hack for event handling + // + // Internet Explorer 8-9 does not support clicks on elements without a set + // `background-color`. We cannot use `filter` since that's not viewed as a + // background color by the browser. Thus, a hack is needed. + // + // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we + // set alpha transparency for the best results possible. + background-color: #000 \9; // IE8 + background-color: rgba(0,0,0,0); // IE9 + } + .active { + margin: 0; + width: 12px; + height: 12px; + background-color: $carousel-indicator-active-bg; + } +} + +// Optional captions +// ----------------------------- +// Hidden by default for smaller viewports +.carousel-caption { + position: absolute; + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: $carousel-caption-color; + text-align: center; + text-shadow: $carousel-text-shadow; + & .btn { + text-shadow: none; // No shadow for button elements in carousel-caption + } +} + + +// Scale up controls for tablets and up +@media screen and (min-width: $screen-sm-min) { + + // Scale up the controls a smidge + .carousel-control { + .glyphicon-chevron-left, + .glyphicon-chevron-right, + .icon-prev, + .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .glyphicon-chevron-left, + .icon-prev { + margin-left: -15px; + } + .glyphicon-chevron-right, + .icon-next { + margin-right: -15px; + } + } + + // Show and left align the captions + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + + // Move up the indicators + .carousel-indicators { + bottom: 20px; + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_close.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_close.scss new file mode 100644 index 000000000..62ce30fa3 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_close.scss @@ -0,0 +1,35 @@ +// +// Close icons +// -------------------------------------------------- + + +.close { + float: right; + font-size: ($font-size-base * 1.5); + font-weight: $close-font-weight; + line-height: 1; + color: $close-color; + text-shadow: $close-text-shadow; + @include opacity(.2); + + &:hover, + &:focus { + color: $close-color; + text-decoration: none; + cursor: pointer; + @include opacity(.5); + } + + // [converter] extracted button& to button.close +} + +// Additional properties for button version +// iOS requires the button element instead of an anchor tag. +// If you want the anchor version, it requires `href="#"`. +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_code.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_code.scss new file mode 100644 index 000000000..3a434b946 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_code.scss @@ -0,0 +1,68 @@ +// +// Code (inline and block) +// -------------------------------------------------- + + +// Inline and block code styles +code, +kbd, +pre, +samp { + font-family: $font-family-monospace; +} + +// Inline code +code { + padding: 2px 4px; + font-size: 90%; + color: $code-color; + background-color: $code-bg; + border-radius: $border-radius-base; +} + +// User input typically entered via keyboard +kbd { + padding: 2px 4px; + font-size: 90%; + color: $kbd-color; + background-color: $kbd-bg; + border-radius: $border-radius-small; + box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); + + kbd { + padding: 0; + font-size: 100%; + box-shadow: none; + } +} + +// Blocks of code +pre { + display: block; + padding: calc(($line-height-computed - 100%) / 2); + margin: 0 0 calc($line-height-computed / 2); + font-size: ($font-size-base - 1); // 14px to 13px + line-height: $line-height-base; + word-break: break-all; + word-wrap: break-word; + color: $pre-color; + background-color: $pre-bg; + border: 1px solid $pre-border-color; + border-radius: $border-radius-base; + + // Account for some code outputs that place code tags in pre tags + code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; + } +} + +// Enable scrollable blocks of code +.pre-scrollable { + max-height: $pre-scrollable-max-height; + overflow-y: scroll; +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_component-animations.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_component-animations.scss new file mode 100644 index 000000000..8c3fd07a2 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_component-animations.scss @@ -0,0 +1,35 @@ +// +// Component animations +// -------------------------------------------------- + +// Heads up! +// +// We don't use the `.opacity()` mixin here since it causes a bug with text +// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. + +.fade { + opacity: 0; + @include transition(opacity .15s linear); + &.in { + opacity: 1; + } +} + +.collapse { + display: none; + + &.in { display: block; } + // [converter] extracted tr&.in to tr.collapse.in + // [converter] extracted tbody&.in to tbody.collapse.in +} + +tr.collapse.in { display: table-row; } + +tbody.collapse.in { display: table-row-group; } + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + @include transition(height .35s ease); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_dropdowns.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_dropdowns.scss new file mode 100644 index 000000000..df50ec0cb --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_dropdowns.scss @@ -0,0 +1,214 @@ +// +// Dropdown menus +// -------------------------------------------------- + + +// Dropdown arrow/caret +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: $caret-width-base solid; + border-right: $caret-width-base solid transparent; + border-left: $caret-width-base solid transparent; +} + +// The dropdown wrapper (div) +.dropdown { + position: relative; +} + +// Prevent the focus on the dropdown toggle when closing dropdowns +.dropdown-toggle:focus { + outline: 0; +} + +// The dropdown menu (ul) +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: $zindex-dropdown; + display: none; // none by default, but block on "open" of the menu + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; // override default ul + list-style: none; + font-size: $font-size-base; + text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) + background-color: $dropdown-bg; + border: 1px solid $dropdown-fallback-border; // IE8 fallback + border: 1px solid $dropdown-border; + border-radius: $border-radius-base; + @include box-shadow(0 6px 12px rgba(0,0,0,.175)); + background-clip: padding-box; + + // Aligns the dropdown menu to right + // + // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` + &.pull-right { + right: 0; + left: auto; + } + + // Dividers (basically an hr) within the dropdown + .divider { + @include nav-divider($dropdown-divider-bg); + } + + // Links within the dropdown menu + > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: $line-height-base; + color: $dropdown-link-color; + white-space: nowrap; // prevent links from randomly breaking onto new lines + } +} + +// Hover/Focus state +.dropdown-menu > li > a { + &:hover, + &:focus { + text-decoration: none; + color: $dropdown-link-hover-color; + background-color: $dropdown-link-hover-bg; + } +} + +// Active state +.dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: $dropdown-link-active-color; + text-decoration: none; + outline: 0; + background-color: $dropdown-link-active-bg; + } +} + +// Disabled state +// +// Gray out text and ensure the hover/focus state remains gray + +.dropdown-menu > .disabled > a { + &, + &:hover, + &:focus { + color: $dropdown-link-disabled-color; + } +} +// Nuke hover/focus effects +.dropdown-menu > .disabled > a { + &:hover, + &:focus { + text-decoration: none; + background-color: transparent; + background-image: none; // Remove CSS gradient + @include reset-filter(); + cursor: not-allowed; + } +} + +// Open state for the dropdown +.open { + // Show the menu + > .dropdown-menu { + display: block; + } + + // Remove the outline when :focus is triggered + > a { + outline: 0; + } +} + +// Menu positioning +// +// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown +// menu with the parent. +.dropdown-menu-right { + left: auto; // Reset the default from `.dropdown-menu` + right: 0; +} +// With v3, we enabled auto-flipping if you have a dropdown within a right +// aligned nav component. To enable the undoing of that, we provide an override +// to restore the default dropdown menu alignment. +// +// This is only for left-aligning a dropdown menu within a `.navbar-right` or +// `.pull-right` nav component. +.dropdown-menu-left { + left: 0; + right: auto; +} + +// Dropdown section headers +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: $font-size-small; + line-height: $line-height-base; + color: $dropdown-header-color; + white-space: nowrap; // as with > li > a +} + +// Backdrop to catch body clicks on mobile, etc. +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: ($zindex-dropdown - 10); +} + +// Right aligned dropdowns +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// +// Just add .dropup after the standard .dropdown class and you're set, bro. +// TODO: abstract this so that the navbar fixed styles are not placed here? + +.dropup, +.navbar-fixed-bottom .dropdown { + // Reverse the caret + .caret { + border-top: 0; + border-bottom: $caret-width-base solid; + content: ""; + } + // Different positioning for bottom up menu + .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; + } +} + + +// Component alignment +// +// Reiterate per navbar.less and the modified component alignment there. + +@media (min-width: $grid-float-breakpoint) { + .navbar-right { + .dropdown-menu { + right: 0; left: auto; + } + // Necessary for overrides of the default right aligned menu. + // Will remove come v4 in all likelihood. + .dropdown-menu-left { + left: 0; right: auto; + } + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_forms.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_forms.scss new file mode 100644 index 000000000..3aaf044c2 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_forms.scss @@ -0,0 +1,540 @@ +// +// Forms +// -------------------------------------------------- + + +// Normalize non-controls +// +// Restyle and baseline non-control form elements. + +fieldset { + padding: 0; + margin: 0; + border: 0; + // Chrome and Firefox set a min-width: min-content; on fieldsets, + // so we reset that to ensure it behaves more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359. + min-width: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: $line-height-computed; + font-size: ($font-size-base * 1.5); + line-height: inherit; + color: $legend-color; + border: 0; + border-bottom: 1px solid $legend-border-color; +} + +label { + display: inline-block; + max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) + margin-bottom: 5px; + font-weight: normal; +} + + +// Normalize form controls +// +// While most of our form styles require extra classes, some basic normalization +// is required to ensure optimum display with or without those classes to better +// address browser inconsistencies. + +// Override content-box in Normalize (isnt specific enough) +input[type="search"] { + @include box-sizing(border-box); +} + +// Position radios and checkboxes better +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; // IE8-9 + line-height: normal; +} + +// Set the height of file controls to match text inputs +input[type="file"] { + display: block; +} + +// Make range inputs behave like textual form controls +input[type="range"] { + display: block; + width: 100%; +} + +// Make multiple select elements height not fixed +select[multiple], +select[size] { + height: auto; +} + +// Focus for file, radio, and checkbox +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + @include tab-focus(); +} + +// Adjust output element +output { + display: block; + padding-top: ($padding-base-vertical + 1); + font-size: $font-size-base; + line-height: $line-height-base; + color: $input-color; +} + + +// Common form controls +// +// Shared size and type resets for form controls. Apply `.form-control` to any +// of the following form controls: +// +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"] +/* .form-control */ +{ + display: block; + width: 100%; + height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + padding: $padding-base-vertical $padding-base-horizontal; + font-size: $font-size-base; + line-height: $line-height-base; + color: $input-color; + background-color: $input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid $input-border; + border-radius: $input-border-radius; + text-overflow: ellipsis; + max-width:348px; + //@include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + @include transition(border-color ease-in-out .1s, box-shadow ease-in-out .1s); + + // Customize the `:focus` state to imitate native WebKit styles. + @include form-control-focus(); + + // Placeholder + @include placeholder(); + + // Disabled and read-only inputs + // + // HTML5 says that controls under a fieldset > legend:first-child wont be + // disabled if the fieldset is disabled. Due to implementation difficulty, we + // dont honor that edge case; we style them as disabled anyway. + &[disabled], + &[readonly], + fieldset[disabled] & { + cursor: not-allowed; + background-color: $input-bg-disabled; + opacity: 1; // iOS fix for unreadable disabled content + } + + // [converter] extracted textarea& to textarea.form-control +} + +// Reset height for `textarea`s +textarea{ + height: auto; +} + + +// Search inputs in iOS +// +// This overrides the extra rounded corners on search inputs in iOS so that our +// `.form-control` class can properly style them. Note that this cannot simply +// be added to `.form-control` as its not specific enough. For details, see +// https://github.com/twbs/bootstrap/issues/11586. + +input[type="search"] { + -webkit-appearance: none; +} + + +// Special styles for iOS temporal inputs +// +// In Mobile Safari, setting `display: block` on temporal inputs causes the +// text within the input to become vertically misaligned. +// As a workaround, we set a pixel line-height that matches the +// given height of the input. Since this fucks up everything else, we have to +// appropriately reset it for Internet Explorer and the size variations. + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + line-height: $input-height-base; + // IE8+ misaligns the text within date inputs, so we reset + line-height: $line-height-base #{\0}; + + &.input-sm { + line-height: $input-height-small; + } + &.input-lg { + line-height: $input-height-large; + } +} + + +// Form groups +// +// Designed to help with the organization and spacing of vertical forms. For +// horizontal forms, use the predefined grid classes. + +.form-group { + margin-bottom: 15px; +} + + +// Checkboxes and radios +// +// Indent the labels to position radios/checkboxes as hanging controls. + +.radio, +.checkbox { + position: relative; + display: block; + min-height: $line-height-computed; // clear the floating input if there is no label text + margin-top: 10px; + margin-bottom: 10px; + + label { + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; + } +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-left: -20px; + margin-top: 4px \9; +} + +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing +} + +// Radios and checkboxes on same line +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; // space out consecutive inline controls +} + +// Apply same disabled cursor tweak as for inputs +// Some special care is needed because Star + +// Import the fonts +@font-face { + font-family: 'Glyphicons Halflings'; + src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot'), '#{$icon-font-path}#{$icon-font-name}.eot')); + src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot?#iefix'), '#{$icon-font-path}#{$icon-font-name}.eot?#iefix')) format('embedded-opentype'), + url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.woff'), '#{$icon-font-path}#{$icon-font-name}.woff')) format('woff'), + url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.ttf'), '#{$icon-font-path}#{$icon-font-name}.ttf')) format('truetype'), + url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}'), '#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}')) format('svg'); +} + +// Catchall baseclass +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// Individual icons +.glyphicon-asterisk { &:before { content: "\2a"; } } +.glyphicon-plus { &:before { content: "\2b"; } } +.glyphicon-euro { &:before { content: "\20ac"; } } +.glyphicon-minus { &:before { content: "\2212"; } } +.glyphicon-cloud { &:before { content: "\2601"; } } +.glyphicon-envelope { &:before { content: "\2709"; } } +.glyphicon-pencil { &:before { content: "\270f"; } } +.glyphicon-glass { &:before { content: "\e001"; } } +.glyphicon-music { &:before { content: "\e002"; } } +.glyphicon-search { &:before { content: "\e003"; } } +.glyphicon-heart { &:before { content: "\e005"; } } +.glyphicon-star { &:before { content: "\e006"; } } +.glyphicon-star-empty { &:before { content: "\e007"; } } +.glyphicon-user { &:before { content: "\e008"; } } +.glyphicon-film { &:before { content: "\e009"; } } +.glyphicon-th-large { &:before { content: "\e010"; } } +.glyphicon-th { &:before { content: "\e011"; } } +.glyphicon-th-list { &:before { content: "\e012"; } } +.glyphicon-ok { &:before { content: "\e013"; } } +.glyphicon-remove { &:before { content: "\e014"; } } +.glyphicon-zoom-in { &:before { content: "\e015"; } } +.glyphicon-zoom-out { &:before { content: "\e016"; } } +.glyphicon-off { &:before { content: "\e017"; } } +.glyphicon-signal { &:before { content: "\e018"; } } +.glyphicon-cog { &:before { content: "\e019"; } } +.glyphicon-trash { &:before { content: "\e020"; } } +.glyphicon-home { &:before { content: "\e021"; } } +.glyphicon-file { &:before { content: "\e022"; } } +.glyphicon-time { &:before { content: "\e023"; } } +.glyphicon-road { &:before { content: "\e024"; } } +.glyphicon-download-alt { &:before { content: "\e025"; } } +.glyphicon-download { &:before { content: "\e026"; } } +.glyphicon-upload { &:before { content: "\e027"; } } +.glyphicon-inbox { &:before { content: "\e028"; } } +.glyphicon-play-circle { &:before { content: "\e029"; } } +.glyphicon-repeat { &:before { content: "\e030"; } } +.glyphicon-refresh { &:before { content: "\e031"; } } +.glyphicon-list-alt { &:before { content: "\e032"; } } +.glyphicon-lock { &:before { content: "\e033"; } } +.glyphicon-flag { &:before { content: "\e034"; } } +.glyphicon-headphones { &:before { content: "\e035"; } } +.glyphicon-volume-off { &:before { content: "\e036"; } } +.glyphicon-volume-down { &:before { content: "\e037"; } } +.glyphicon-volume-up { &:before { content: "\e038"; } } +.glyphicon-qrcode { &:before { content: "\e039"; } } +.glyphicon-barcode { &:before { content: "\e040"; } } +.glyphicon-tag { &:before { content: "\e041"; } } +.glyphicon-tags { &:before { content: "\e042"; } } +.glyphicon-book { &:before { content: "\e043"; } } +.glyphicon-bookmark { &:before { content: "\e044"; } } +.glyphicon-print { &:before { content: "\e045"; } } +.glyphicon-camera { &:before { content: "\e046"; } } +.glyphicon-font { &:before { content: "\e047"; } } +.glyphicon-bold { &:before { content: "\e048"; } } +.glyphicon-italic { &:before { content: "\e049"; } } +.glyphicon-text-height { &:before { content: "\e050"; } } +.glyphicon-text-width { &:before { content: "\e051"; } } +.glyphicon-align-left { &:before { content: "\e052"; } } +.glyphicon-align-center { &:before { content: "\e053"; } } +.glyphicon-align-right { &:before { content: "\e054"; } } +.glyphicon-align-justify { &:before { content: "\e055"; } } +.glyphicon-list { &:before { content: "\e056"; } } +.glyphicon-indent-left { &:before { content: "\e057"; } } +.glyphicon-indent-right { &:before { content: "\e058"; } } +.glyphicon-facetime-video { &:before { content: "\e059"; } } +.glyphicon-picture { &:before { content: "\e060"; } } +.glyphicon-map-marker { &:before { content: "\e062"; } } +.glyphicon-adjust { &:before { content: "\e063"; } } +.glyphicon-tint { &:before { content: "\e064"; } } +.glyphicon-edit { &:before { content: "\e065"; } } +.glyphicon-share { &:before { content: "\e066"; } } +.glyphicon-check { &:before { content: "\e067"; } } +.glyphicon-move { &:before { content: "\e068"; } } +.glyphicon-step-backward { &:before { content: "\e069"; } } +.glyphicon-fast-backward { &:before { content: "\e070"; } } +.glyphicon-backward { &:before { content: "\e071"; } } +.glyphicon-play { &:before { content: "\e072"; } } +.glyphicon-pause { &:before { content: "\e073"; } } +.glyphicon-stop { &:before { content: "\e074"; } } +.glyphicon-forward { &:before { content: "\e075"; } } +.glyphicon-fast-forward { &:before { content: "\e076"; } } +.glyphicon-step-forward { &:before { content: "\e077"; } } +.glyphicon-eject { &:before { content: "\e078"; } } +.glyphicon-chevron-left { &:before { content: "\e079"; } } +.glyphicon-chevron-right { &:before { content: "\e080"; } } +.glyphicon-plus-sign { &:before { content: "\e081"; } } +.glyphicon-minus-sign { &:before { content: "\e082"; } } +.glyphicon-remove-sign { &:before { content: "\e083"; } } +.glyphicon-ok-sign { &:before { content: "\e084"; } } +.glyphicon-question-sign { &:before { content: "\e085"; } } +.glyphicon-info-sign { &:before { content: "\e086"; } } +.glyphicon-screenshot { &:before { content: "\e087"; } } +.glyphicon-remove-circle { &:before { content: "\e088"; } } +.glyphicon-ok-circle { &:before { content: "\e089"; } } +.glyphicon-ban-circle { &:before { content: "\e090"; } } +.glyphicon-arrow-left { &:before { content: "\e091"; } } +.glyphicon-arrow-right { &:before { content: "\e092"; } } +.glyphicon-arrow-up { &:before { content: "\e093"; } } +.glyphicon-arrow-down { &:before { content: "\e094"; } } +.glyphicon-share-alt { &:before { content: "\e095"; } } +.glyphicon-resize-full { &:before { content: "\e096"; } } +.glyphicon-resize-small { &:before { content: "\e097"; } } +.glyphicon-exclamation-sign { &:before { content: "\e101"; } } +.glyphicon-gift { &:before { content: "\e102"; } } +.glyphicon-leaf { &:before { content: "\e103"; } } +.glyphicon-fire { &:before { content: "\e104"; } } +.glyphicon-eye-open { &:before { content: "\e105"; } } +.glyphicon-eye-close { &:before { content: "\e106"; } } +.glyphicon-warning-sign { &:before { content: "\e107"; } } +.glyphicon-plane { &:before { content: "\e108"; } } +.glyphicon-calendar { &:before { content: "\e109"; } } +.glyphicon-random { &:before { content: "\e110"; } } +.glyphicon-comment { &:before { content: "\e111"; } } +.glyphicon-magnet { &:before { content: "\e112"; } } +.glyphicon-chevron-up { &:before { content: "\e113"; } } +.glyphicon-chevron-down { &:before { content: "\e114"; } } +.glyphicon-retweet { &:before { content: "\e115"; } } +.glyphicon-shopping-cart { &:before { content: "\e116"; } } +.glyphicon-folder-close { &:before { content: "\e117"; } } +.glyphicon-folder-open { &:before { content: "\e118"; } } +.glyphicon-resize-vertical { &:before { content: "\e119"; } } +.glyphicon-resize-horizontal { &:before { content: "\e120"; } } +.glyphicon-hdd { &:before { content: "\e121"; } } +.glyphicon-bullhorn { &:before { content: "\e122"; } } +.glyphicon-bell { &:before { content: "\e123"; } } +.glyphicon-certificate { &:before { content: "\e124"; } } +.glyphicon-thumbs-up { &:before { content: "\e125"; } } +.glyphicon-thumbs-down { &:before { content: "\e126"; } } +.glyphicon-hand-right { &:before { content: "\e127"; } } +.glyphicon-hand-left { &:before { content: "\e128"; } } +.glyphicon-hand-up { &:before { content: "\e129"; } } +.glyphicon-hand-down { &:before { content: "\e130"; } } +.glyphicon-circle-arrow-right { &:before { content: "\e131"; } } +.glyphicon-circle-arrow-left { &:before { content: "\e132"; } } +.glyphicon-circle-arrow-up { &:before { content: "\e133"; } } +.glyphicon-circle-arrow-down { &:before { content: "\e134"; } } +.glyphicon-globe { &:before { content: "\e135"; } } +.glyphicon-wrench { &:before { content: "\e136"; } } +.glyphicon-tasks { &:before { content: "\e137"; } } +.glyphicon-filter { &:before { content: "\e138"; } } +.glyphicon-briefcase { &:before { content: "\e139"; } } +.glyphicon-fullscreen { &:before { content: "\e140"; } } +.glyphicon-dashboard { &:before { content: "\e141"; } } +.glyphicon-paperclip { &:before { content: "\e142"; } } +.glyphicon-heart-empty { &:before { content: "\e143"; } } +.glyphicon-link { &:before { content: "\e144"; } } +.glyphicon-phone { &:before { content: "\e145"; } } +.glyphicon-pushpin { &:before { content: "\e146"; } } +.glyphicon-usd { &:before { content: "\e148"; } } +.glyphicon-gbp { &:before { content: "\e149"; } } +.glyphicon-sort { &:before { content: "\e150"; } } +.glyphicon-sort-by-alphabet { &:before { content: "\e151"; } } +.glyphicon-sort-by-alphabet-alt { &:before { content: "\e152"; } } +.glyphicon-sort-by-order { &:before { content: "\e153"; } } +.glyphicon-sort-by-order-alt { &:before { content: "\e154"; } } +.glyphicon-sort-by-attributes { &:before { content: "\e155"; } } +.glyphicon-sort-by-attributes-alt { &:before { content: "\e156"; } } +.glyphicon-unchecked { &:before { content: "\e157"; } } +.glyphicon-expand { &:before { content: "\e158"; } } +.glyphicon-collapse-down { &:before { content: "\e159"; } } +.glyphicon-collapse-up { &:before { content: "\e160"; } } +.glyphicon-log-in { &:before { content: "\e161"; } } +.glyphicon-flash { &:before { content: "\e162"; } } +.glyphicon-log-out { &:before { content: "\e163"; } } +.glyphicon-new-window { &:before { content: "\e164"; } } +.glyphicon-record { &:before { content: "\e165"; } } +.glyphicon-save { &:before { content: "\e166"; } } +.glyphicon-open { &:before { content: "\e167"; } } +.glyphicon-saved { &:before { content: "\e168"; } } +.glyphicon-import { &:before { content: "\e169"; } } +.glyphicon-export { &:before { content: "\e170"; } } +.glyphicon-send { &:before { content: "\e171"; } } +.glyphicon-floppy-disk { &:before { content: "\e172"; } } +.glyphicon-floppy-saved { &:before { content: "\e173"; } } +.glyphicon-floppy-remove { &:before { content: "\e174"; } } +.glyphicon-floppy-save { &:before { content: "\e175"; } } +.glyphicon-floppy-open { &:before { content: "\e176"; } } +.glyphicon-credit-card { &:before { content: "\e177"; } } +.glyphicon-transfer { &:before { content: "\e178"; } } +.glyphicon-cutlery { &:before { content: "\e179"; } } +.glyphicon-header { &:before { content: "\e180"; } } +.glyphicon-compressed { &:before { content: "\e181"; } } +.glyphicon-earphone { &:before { content: "\e182"; } } +.glyphicon-phone-alt { &:before { content: "\e183"; } } +.glyphicon-tower { &:before { content: "\e184"; } } +.glyphicon-stats { &:before { content: "\e185"; } } +.glyphicon-sd-video { &:before { content: "\e186"; } } +.glyphicon-hd-video { &:before { content: "\e187"; } } +.glyphicon-subtitles { &:before { content: "\e188"; } } +.glyphicon-sound-stereo { &:before { content: "\e189"; } } +.glyphicon-sound-dolby { &:before { content: "\e190"; } } +.glyphicon-sound-5-1 { &:before { content: "\e191"; } } +.glyphicon-sound-6-1 { &:before { content: "\e192"; } } +.glyphicon-sound-7-1 { &:before { content: "\e193"; } } +.glyphicon-copyright-mark { &:before { content: "\e194"; } } +.glyphicon-registration-mark { &:before { content: "\e195"; } } +.glyphicon-cloud-download { &:before { content: "\e197"; } } +.glyphicon-cloud-upload { &:before { content: "\e198"; } } +.glyphicon-tree-conifer { &:before { content: "\e199"; } } +.glyphicon-tree-deciduous { &:before { content: "\e200"; } } diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_grid.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_grid.scss new file mode 100644 index 000000000..f71f8b901 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_grid.scss @@ -0,0 +1,84 @@ +// +// Grid system +// -------------------------------------------------- + + +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. + +.container { + @include container-fixed(); + + @media (min-width: $screen-sm-min) { + width: $container-sm; + } + @media (min-width: $screen-md-min) { + width: $container-md; + } + @media (min-width: $screen-lg-min) { + width: $container-lg; + } +} + + +// Fluid container +// +// Utilizes the mixin meant for fixed width containers, but without any defined +// width for fluid, full width layouts. + +.container-fluid { + @include container-fixed(); +} + + +// Row +// +// Rows contain and clear the floats of your columns. + +.row { + @include make-row(); +} + + +// Columns +// +// Common styles for small and large grid columns + +@include make-grid-columns(); + + +// Extra small grid +// +// Columns, offsets, pushes, and pulls for extra small devices like +// smartphones. + +@include make-grid(xs); + + +// Small grid +// +// Columns, offsets, pushes, and pulls for the small device range, from phones +// to tablets. + +@media (min-width: $screen-sm-min) { + @include make-grid(sm); +} + + +// Medium grid +// +// Columns, offsets, pushes, and pulls for the desktop device range. + +@media (min-width: $screen-md-min) { + @include make-grid(md); +} + + +// Large grid +// +// Columns, offsets, pushes, and pulls for the large desktop device range. + +@media (min-width: $screen-lg-min) { + @include make-grid(lg); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_input-groups.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_input-groups.scss new file mode 100644 index 000000000..f56322e3b --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_input-groups.scss @@ -0,0 +1,166 @@ +// +// Input groups +// -------------------------------------------------- + +// Base styles +// ------------------------- +.input-group { + position: relative; // For dropdowns + display: table; + border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table + + // Undo padding and float of grid classes + &[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; + } + + .form-control { + // Ensure that the input is always above the *appended* addon button for + // proper border colors. + position: relative; + z-index: 2; + + // IE9 fubars the placeholder attribute in text inputs and the arrows on + // select elements in input groups. To fix it, we float the input. Details: + // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 + float: left; + + width: 100%; + margin-bottom: 0; + } +} + +// Sizing options +// +// Remix the default form control sizing classes into new ones for easier +// manipulation. + +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + @extend .input-lg; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + @extend .input-sm; +} + + +// Display as table-cell +// ------------------------- +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; + + &:not(:first-child):not(:last-child) { + border-radius: 0; + } +} +// Addon and addon wrapper for buttons +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; // Match the inputs +} + +// Text input groups +// ------------------------- +.input-group-addon { + padding: $padding-base-vertical $padding-base-horizontal; + font-size: $font-size-base; + font-weight: normal; + line-height: 1; + color: $input-color; + text-align: center; + background-color: $input-group-addon-bg; + border: 1px solid $input-group-addon-border-color; + border-radius: $border-radius-base; + + // Sizing + &.input-sm { + padding: $padding-small-vertical $padding-small-horizontal; + font-size: $font-size-small; + border-radius: $border-radius-small; + } + &.input-lg { + padding: $padding-large-vertical $padding-large-horizontal; + font-size: $font-size-large; + border-radius: $border-radius-large; + } + + // Nuke default margins from checkboxes and radios to vertically center within. + input[type="radio"], + input[type="checkbox"] { + margin-top: 0; + } +} + +// Reset rounded corners +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + @include border-right-radius(0); +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + @include border-left-radius(0); +} +.input-group-addon:last-child { + border-left: 0; +} + +// Button input groups +// ------------------------- +.input-group-btn { + position: relative; + // Jankily prevent input button groups from wrapping with `white-space` and + // `font-size` in combination with `inline-block` on buttons. + font-size: 0; + white-space: nowrap; + + // Negative margin for spacing, position for bringing hovered/focused/activated + // element above the siblings. + > .btn { + position: relative; + + .btn { + margin-left: -1px; + } + // Bring the "active" button to the front + &:hover, + &:focus, + &:active { + z-index: 2; + } + } + + // Negative margin to only have a 1px border between the two + &:first-child { + > .btn, + > .btn-group { + margin-right: -1px; + } + } + &:last-child { + > .btn, + > .btn-group { + margin-left: -1px; + } + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_jumbotron.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_jumbotron.scss new file mode 100644 index 000000000..110c97f54 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_jumbotron.scss @@ -0,0 +1,48 @@ +// +// Jumbotron +// -------------------------------------------------- + + +.jumbotron { + padding: $jumbotron-padding; + margin-bottom: $jumbotron-padding; + color: $jumbotron-color; + background-color: $jumbotron-bg; + + h1, + .h1 { + color: $jumbotron-heading-color; + } + p { + margin-bottom: calc($jumbotron-padding / 2); + font-size: $jumbotron-font-size; + font-weight: 200; + } + + > hr { + border-top-color: darken($jumbotron-bg, 10%); + } + + .container & { + border-radius: $border-radius-large; // Only round corners at higher resolutions if contained in a container + } + + .container { + max-width: 100%; + } + + @media screen and (min-width: $screen-sm-min) { + padding-top: ($jumbotron-padding * 1.6); + padding-bottom: ($jumbotron-padding * 1.6); + + .container & { + padding-left: ($jumbotron-padding * 2); + padding-right: ($jumbotron-padding * 2); + } + + h1, + .h1 { + font-size: ($font-size-base * 4.5); + } + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_labels.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_labels.scss new file mode 100644 index 000000000..42ed6ea12 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_labels.scss @@ -0,0 +1,66 @@ +// +// Labels +// -------------------------------------------------- + +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: $label-color; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; + + // [converter] extracted a& to a.label + + // Empty labels collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for labels in buttons + .btn & { + position: relative; + top: -1px; + } +} + +// Add hover effects, but only for links +a.label { + &:hover, + &:focus { + color: $label-link-hover-color; + text-decoration: none; + cursor: pointer; + } +} + +// Colors +// Contextual variations (linked labels get darker on :hover) + +.label-default { + @include label-variant($label-default-bg); +} + +.label-primary { + @include label-variant($label-primary-bg); +} + +.label-success { + @include label-variant($label-success-bg); +} + +.label-info { + @include label-variant($label-info-bg); +} + +.label-warning { + @include label-variant($label-warning-bg); +} + +.label-danger { + @include label-variant($label-danger-bg); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_list-group.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_list-group.scss new file mode 100644 index 000000000..63027b880 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_list-group.scss @@ -0,0 +1,159 @@ +// +// List groups +// -------------------------------------------------- + + +// Base class +// +// Easily usable on
      ,
        , or
        . + +.list-group { + // No need to set list-style: none; since .list-group-item is block level + margin-bottom: 20px; + padding-left: 0; // reset padding because ul and ol +} + + +// Individual list items +// +// Use on `li`s or `div`s within the `.list-group` parent. + +.list-group-item { + position: relative; + display: block; + padding: 6px 8px; + // Place the border on the list items and negative margin up for better styling + margin-bottom: -1px; + background-color: $list-group-bg; + //border: 1px solid $list-group-border; + + // Round the first and last items + &:first-child { + } + &:last-child { + margin-bottom: 0; + } + + // Align badges within list items + > .badge { + float: right; + } + > .badge + .badge { + margin-right: 5px; + } +} + + +// Linked list items +// +// Use anchor elements instead of `li`s or `div`s to create linked list items. +// Includes an extra `.active` modifier class for showing selected items. + +a.list-group-item { + color: $list-group-link-color; + border-radius: 0; + + .list-group-item-heading { + color: $list-group-link-heading-color; + } + + // Hover state + &:hover, + &:focus { + text-decoration: none; + color: $list-group-link-hover-color; + background-color: $list-group-hover-bg; + + &:before{ + background: $list-group-active-border; + content: ""; + height: 42px; + left: 0; + position: absolute; + top:0; + width: 3px; + } + } +} + +.list-group-item { + // Disabled state + &.disabled, + &.disabled:hover, + &.disabled:focus { + background-color: $list-group-disabled-bg; + color: $list-group-disabled-color; + + // Force color to inherit for custom content + .list-group-item-heading { + color: inherit; + } + .list-group-item-text { + color: $list-group-disabled-text-color; + } + } + + // Active class on item itself, not parent + &.active, + &.active:hover, + &.active:focus { + z-index: 2; // Place active items above their siblings for proper border styling + + &:before{ + background: $list-group-active-border; + content: ""; + height: 42px; + left: 0; + position: absolute; + top:0px; + width: 3px; + } + + // Force color to inherit for custom content + .list-group-item-heading, + .list-group-item-heading > small, + .list-group-item-heading > .small { + color: inherit; + } + .list-group-item-text { + color: $list-group-active-text-color; + } + + +.collapse > .list-group-item{ + &:before{ + background: $list-group-active-border; + content: ""; + height: 42px; + left: 0; + position: absolute; + top:0px; + width: 3px; + } + } + } +} + + +// Contextual variants +// +// Add modifier classes to change text and background color on individual items. +// Organizationally, this must come after the `:hover` states. + +@include list-group-item-variant(success, $state-success-bg, $state-success-text); +@include list-group-item-variant(info, $state-info-bg, $state-info-text); +@include list-group-item-variant(warning, $state-warning-bg, $state-warning-text); +@include list-group-item-variant(danger, $state-danger-bg, $state-danger-text); + + +// Custom content options +// +// Extra classes for creating well-formatted content within `.list-group-item`s. + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_media.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_media.scss new file mode 100644 index 000000000..5ad22cd6d --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_media.scss @@ -0,0 +1,56 @@ +// Media objects +// Source: http://stubbornella.org/content/?p=497 +// -------------------------------------------------- + + +// Common styles +// ------------------------- + +// Clear the floats +.media, +.media-body { + overflow: hidden; + zoom: 1; +} + +// Proper spacing between instances of .media +.media, +.media .media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} + +// For images and videos, set to block +.media-object { + display: block; +} + +// Reset margins on headings for tighter default spacing +.media-heading { + margin: 0 0 5px; +} + + +// Media image alignment +// ------------------------- + +.media { + > .pull-left { + margin-right: 10px; + } + > .pull-right { + margin-left: 10px; + } +} + + +// Media list variation +// ------------------------- + +// Undo default ul/ol styles +.media-list { + padding-left: 0; + list-style: none; +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_mixins.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_mixins.scss new file mode 100644 index 000000000..b565f013a --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_mixins.scss @@ -0,0 +1,39 @@ +// Mixins +// -------------------------------------------------- + +// Utilities +@import "mixins/hide-text"; +@import "mixins/opacity"; +@import "mixins/image"; +@import "mixins/labels"; +@import "mixins/reset-filter"; +@import "mixins/resize"; +@import "mixins/responsive-visibility"; +@import "mixins/size"; +@import "mixins/tab-focus"; +@import "mixins/text-emphasis"; +@import "mixins/text-overflow"; +@import "mixins/vendor-prefixes"; + +// Components +@import "mixins/alerts"; +@import "mixins/buttons"; +@import "mixins/panels"; +@import "mixins/pagination"; +@import "mixins/list-group"; +@import "mixins/nav-divider"; +@import "mixins/forms"; +@import "mixins/progress-bar"; +@import "mixins/table-row"; + +// Skins +@import "mixins/background-variant"; +@import "mixins/border-radius"; +@import "mixins/gradients"; + +// Layout +@import "mixins/clearfix"; +@import "mixins/center-block"; +@import "mixins/nav-vertical-align"; +@import "mixins/grid-framework"; +@import "mixins/grid"; diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_modals.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_modals.scss new file mode 100644 index 000000000..df980e55c --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_modals.scss @@ -0,0 +1,150 @@ +// +// Modals +// -------------------------------------------------- + +// .modal-open - body class for killing the scroll +// .modal - container to scroll within +// .modal-dialog - positioning shell for the actual modal +// .modal-content - actual modal w/ bg and corners and shit + +// Kill the scroll on the body +.modal-open { + overflow: hidden; +} + +// Container that the modal scrolls within +.modal { + display: none; + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: $zindex-modal; + -webkit-overflow-scrolling: touch; + + // Prevent Chrome on Windows from adding a focus outline. For details, see + // https://github.com/twbs/bootstrap/pull/10951. + outline: 0; + + // When fading in the modal, animate it to slide down + &.fade .modal-dialog { + @include translate3d(0, -25%, 0); + @include transition-transform(0.3s ease-out); + } + &.in .modal-dialog { @include translate3d(0, 0, 0) } +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +// Shell div to position the modal with bottom padding +.modal-dialog { + position: relative; + width: auto; + margin: 52px+10px 10px 10px 10px; +} + +// Actual modal +.modal-content { + position: relative; + background-color: $modal-content-bg; + border: 1px solid $modal-content-fallback-border-color; //old browsers fallback (ie8 etc) + border: 1px solid $modal-content-border-color; + border-radius: $border-radius-large; + @include box-shadow(0 3px 9px rgba(0,0,0,.5)); + background-clip: padding-box; + // Remove focus outline from opened modal + outline: 0; +} + +// Modal background +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: $zindex-modal-background; + background-color: $modal-backdrop-bg; + // Fade for backdrop + &.fade { @include opacity(0); } + &.in { @include opacity($modal-backdrop-opacity); } +} + +// Modal header +// Top section of the modal w/ title and dismiss +.modal-header { + padding: $modal-title-padding; + border-bottom: 1px solid $modal-header-border-color; + min-height: ($modal-title-padding + $modal-title-line-height); +} +// Close icon +.modal-header .close { + margin-top: -2px; +} + +// Title text within header +.modal-title { + margin: 0; + line-height: $modal-title-line-height; +} + +// Modal body +// Where all modal content resides (sibling of .modal-header and .modal-footer) +.modal-body { + position: relative; + padding: $modal-inner-padding; +} + +// Footer (for actions) +.modal-footer { + padding: $modal-inner-padding; + text-align: right; // right align buttons + border-top: 1px solid $modal-footer-border-color; + @include clearfix(); // clear it in case folks use .pull-* classes on buttons + + // Properly space out buttons + .btn + .btn { + margin-left: 5px; + margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs + } + // but override that for button groups + .btn-group .btn + .btn { + margin-left: -1px; + } + // and override it for block buttons as well + .btn-block + .btn-block { + margin-left: 0; + } +} + +// Measure scrollbar width for padding body during modal show/hide +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +// Scale up the modal +@media (min-width: $screen-sm-min) { + // Automatically set modal's width for larger viewports + .modal-dialog { + width: $modal-md; + margin: 52px+30px auto 30px auto; + } + .modal-content { + @include box-shadow(0 5px 15px rgba(0,0,0,.5)); + } + + // Modal sizes + .modal-sm { width: $modal-sm; } +} + +@media (min-width: $screen-md-min) { + .modal-lg { width: $modal-lg; } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_navbar.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_navbar.scss new file mode 100644 index 000000000..6cee973c3 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_navbar.scss @@ -0,0 +1,658 @@ +// +// Navbars +// -------------------------------------------------- + + +// Wrapper and base class +// +// Provide a static navbar from which we expand to create full-width, fixed, and +// other navbar variations. + +.navbar { + position: relative; + min-height: $navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode) + margin-bottom: $navbar-margin-bottom; + border: 1px solid transparent; + + // Prevent floats from breaking the navbar + @include clearfix(); + + @media (min-width: $grid-float-breakpoint) { + border-radius: $navbar-border-radius; + } +} + + +// Navbar heading +// +// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy +// styling of responsive aspects. + +.navbar-header { + @include clearfix(); + + @media (min-width: $grid-float-breakpoint) { + float: left; + } +} + + +// Navbar collapse (body) +// +// Group your navbar content into this for easy collapsing and expanding across +// various device sizes. By default, this content is collapsed when <768px, but +// will expand past that for a horizontal display. +// +// To start (on mobile devices) the navbar links, forms, and buttons are stacked +// vertically and include a `max-height` to overflow in case you have too much +// content for the user's viewport. + +.navbar-collapse { + overflow-x: visible; + padding-right: $navbar-padding-horizontal; + padding-left: $navbar-padding-horizontal; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255,255,255,.1); + @include clearfix(); + -webkit-overflow-scrolling: touch; + + &.in { + overflow-y: auto; + } + + @media (min-width: $grid-float-breakpoint) { + width: auto; + border-top: 0; + box-shadow: none; + + &.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; // Override default setting + overflow: visible !important; + } + + &.in { + overflow-y: visible; + } + + // Undo the collapse side padding for navbars with containers to ensure + // alignment of right-aligned contents. + .navbar-fixed-top &, + .navbar-static-top &, + .navbar-fixed-bottom & { + padding-left: 0; + padding-right: 0; + } + } +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + .navbar-collapse { + max-height: $navbar-collapse-max-height; + + @media (max-width: $screen-xs-min) and (orientation: landscape) { + max-height: 200px; + } + } +} + + +// Both navbar header and collapse +// +// When a container is present, change the behavior of the header and collapse. + +.container, +.container-fluid { + > .navbar-header, + > .navbar-collapse { + margin-right: -$navbar-padding-horizontal; + margin-left: -$navbar-padding-horizontal; + + @media (min-width: $grid-float-breakpoint) { + margin-right: 0; + margin-left: 0; + } + } +} + + +// +// Navbar alignment options +// +// Display the navbar across the entirety of the page or fixed it to the top or +// bottom of the page. + +// Static top (unfixed, but 100% wide) navbar +.navbar-static-top { + z-index: $zindex-navbar; + border-width: 0 0 1px; + + @media (min-width: $grid-float-breakpoint) { + border-radius: 0; + } +} + +// Fix the top/bottom navbars when screen real estate supports it +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: $zindex-navbar-fixed; + @include translate3d(0, 0, 0); + + // Undo the rounded corners + @media (min-width: $grid-float-breakpoint) { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; // override .navbar defaults + border-width: 1px 0 0; +} + + +// Brand/project name + +.navbar-brand { + float: left; + padding: $navbar-padding-vertical $navbar-padding-horizontal; + font-size: $font-size-large; + height: $navbar-height; + + &:hover, + &:focus { + text-decoration: none; + } + + @media (min-width: $grid-float-breakpoint) { + .navbar > .container &, + .navbar > .container-fluid & { + margin-left: -$navbar-padding-horizontal; + } + } +} + + +// Navbar toggle +// +// Custom button for toggling the `.navbar-collapse`, powered by the collapse +// JavaScript plugin. + +.navbar-toggle { + position: relative; + float: left; + margin-right: $navbar-padding-horizontal; + padding: 9px 10px; + @include navbar-vertical-align(34px); + background-color: transparent; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + border-radius: $border-radius-base; + + // We remove the `outline` here, but later compensate by attaching `:hover` + // styles to `:focus`. + &:focus { + outline: 0; + } + + // Bars + .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; + } + .icon-bar + .icon-bar { + margin-top: 4px; + } + + @media (min-width: $grid-float-breakpoint) { + display: none; + } +} + + +// Navbar nav links +// +// Builds on top of the `.nav` components with its own modifier class to make +// the nav the full height of the horizontal nav (above 768px). + +.navbar-nav { + margin: calc($navbar-padding-vertical / 2) (-$navbar-padding-horizontal); + + > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: $line-height-computed; + } + + @media (max-width: $grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + > li > a, + .dropdown-header { + padding: 5px 15px 5px 25px; + } + > li > a { + line-height: $line-height-computed; + &:hover, + &:focus { + background-image: none; + } + } + } + } + + // Uncollapse the nav + @media (min-width: $grid-float-breakpoint) { + float: left; + margin: 0; + + > li { + float: left; + > a { + padding-top: $navbar-padding-vertical; + padding-bottom: $navbar-padding-vertical; + } + } + + &.navbar-right:last-child { + margin-right: -$navbar-padding-horizontal; + } + } +} + + +// Component alignment +// +// Repurpose the pull utilities as their own navbar utilities to avoid specificity +// issues with parents and chaining. Only do this when the navbar is uncollapsed +// though so that navbar contents properly stack and align in mobile. + +@media (min-width: $grid-float-breakpoint) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + } +} + + +// Navbar form +// +// Extension of the `.form-inline` with some extra flavor for optimum display in +// our navbars. + +.navbar-form { + margin-left: -$navbar-padding-horizontal; + margin-right: -$navbar-padding-horizontal; + padding: 10px 0px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + $shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1); + @include box-shadow($shadow); + + // Mixin behavior for optimum display + @extend .form-inline; + + .form-group { + @media (max-width: $grid-float-breakpoint-max) { + margin-bottom: 5px; + } + } + + // Vertically center in expanded, horizontal navbar + @include navbar-vertical-align($input-height-base); + + // Undo 100% width for pull classes + @media (min-width: $grid-float-breakpoint) { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + @include box-shadow(none); + + // Outdent the form if last child to line up with content down the page + &.navbar-right:last-child { + margin-right: -$navbar-padding-horizontal; + } + } +} + + +// Dropdown menus + +// Menu position and menu carets +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + @include border-top-radius(0); +} +// Menu position and menu caret support for dropups via extra dropup class +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + @include border-bottom-radius(0); +} + + +// Buttons in navbars +// +// Vertically center a button within a navbar (when *not* in a form). + +.navbar-btn { + @include navbar-vertical-align($input-height-base); + + &.btn-sm { + @include navbar-vertical-align($input-height-small); + } + &.btn-xs { + @include navbar-vertical-align(22px); + } +} + + +// Text in navbars +// +// Add a class to make any element properly align itself vertically within the navbars. + +.navbar-text { + @include navbar-vertical-align($line-height-computed); + + @media (min-width: $grid-float-breakpoint) { + float: left; + margin-left: $navbar-padding-horizontal; + margin-right: $navbar-padding-horizontal; + + // Outdent the form if last child to line up with content down the page + &.navbar-right:last-child { + margin-right: 0; + } + } +} + +// Alternate navbars +// -------------------------------------------------- + +// Default navbar +.navbar-default { + background-color: $navbar-default-bg; + border-color: $navbar-default-border; + + .navbar-brand { + color: $navbar-default-brand-color; + &:hover, + &:focus { + color: $navbar-default-brand-hover-color; + background-color: $navbar-default-brand-hover-bg; + } + } + + .navbar-text { + color: $navbar-default-color; + } + + .navbar-nav { + > li > a { + color: $navbar-default-link-color; + + &:hover, + &:focus { + color: $navbar-default-link-hover-color; + background-color: $navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: $navbar-default-link-active-color; + background-color: $navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: $navbar-default-link-disabled-color; + background-color: $navbar-default-link-disabled-bg; + } + } + } + + .navbar-toggle { + border-color: $navbar-default-toggle-border-color; + &:hover, + &:focus { + background-color: $navbar-default-toggle-hover-bg; + } + .icon-bar { + background-color: $navbar-default-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: $navbar-default-border; + } + + // Dropdown menu items + .navbar-nav { + // Remove background color from open dropdown + > .open > a { + &, + &:hover, + &:focus { + background-color: $navbar-default-link-active-bg; + color: $navbar-default-link-active-color; + } + } + + @media (max-width: $grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + > li > a { + color: $navbar-default-link-color; + &:hover, + &:focus { + color: $navbar-default-link-hover-color; + background-color: $navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: $navbar-default-link-active-color; + background-color: $navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: $navbar-default-link-disabled-color; + background-color: $navbar-default-link-disabled-bg; + } + } + } + } + } + + + // Links in navbars + // + // Add a class to ensure links outside the navbar nav are colored correctly. + + .navbar-link { + color: $navbar-default-link-color; + &:hover { + color: $navbar-default-link-hover-color; + } + } + + .btn-link { + color: $navbar-default-link-color; + &:hover, + &:focus { + color: $navbar-default-link-hover-color; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: $navbar-default-link-disabled-color; + } + } + } +} + +// Inverse navbar + +.navbar-inverse { + background-color: $navbar-inverse-bg; + border-color: $navbar-inverse-border; + + .navbar-brand { + color: $navbar-inverse-brand-color; + &:hover, + &:focus { + color: $navbar-inverse-brand-hover-color; + background-color: $navbar-inverse-brand-hover-bg; + } + } + + .navbar-text { + color: $navbar-inverse-color; + } + + .navbar-nav { + > li > a { + color: $navbar-inverse-link-color; + + &:hover, + &:focus { + color: $navbar-inverse-link-hover-color; + background-color: $navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: $navbar-inverse-link-active-color; + background-color: $navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: $navbar-inverse-link-disabled-color; + background-color: $navbar-inverse-link-disabled-bg; + } + } + } + + // Darken the responsive nav toggle + .navbar-toggle { + border-color: $navbar-inverse-toggle-border-color; + &:hover, + &:focus { + background-color: $navbar-inverse-toggle-hover-bg; + } + .icon-bar { + background-color: $navbar-inverse-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: darken($navbar-inverse-bg, 7%); + } + + // Dropdowns + .navbar-nav { + > .open > a { + &, + &:hover, + &:focus { + background-color: $navbar-inverse-link-active-bg; + color: $navbar-inverse-link-active-color; + } + } + + @media (max-width: $grid-float-breakpoint-max) { + // Dropdowns get custom display + .open .dropdown-menu { + > .dropdown-header { + border-color: $navbar-inverse-border; + } + .divider { + background-color: $navbar-inverse-border; + } + > li > a { + color: $navbar-inverse-link-color; + &:hover, + &:focus { + color: $navbar-inverse-link-hover-color; + background-color: $navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: $navbar-inverse-link-active-color; + background-color: $navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: $navbar-inverse-link-disabled-color; + background-color: $navbar-inverse-link-disabled-bg; + } + } + } + } + } + + .navbar-link { + color: $navbar-inverse-link-color; + &:hover { + color: $navbar-inverse-link-hover-color; + } + } + + .btn-link { + color: $navbar-inverse-link-color; + &:hover, + &:focus { + color: $navbar-inverse-link-hover-color; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: $navbar-inverse-link-disabled-color; + } + } + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_navs.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_navs.scss new file mode 100644 index 000000000..d9957806e --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_navs.scss @@ -0,0 +1,239 @@ +// +// Navs +// -------------------------------------------------- + + +// Base class +// -------------------------------------------------- + +.nav { + margin-bottom: 0; + padding-left: 0; // Override default ul/ol + list-style: none; + @include clearfix(); + + > li { + position: relative; + display: block; + + > a { + position: relative; + display: block; + padding: $nav-link-padding; + &:hover, + &:focus { + text-decoration: none; + background-color: $nav-link-hover-bg; + } + } + + // Disabled state sets text to gray and nukes hover/tab effects + &.disabled > a { + color: $nav-disabled-link-color; + + &:hover, + &:focus { + color: $nav-disabled-link-hover-color; + text-decoration: none; + background-color: transparent; + cursor: not-allowed; + } + } + } + + // Open dropdowns + .open > a { + &, + &:hover, + &:focus { + background-color: $nav-link-hover-bg; + border-color: $link-color; + } + } + + // Nav dividers (deprecated with v3.0.1) + // + // This should have been removed in v3 with the dropping of `.nav-list`, but + // we missed it. We don't currently support this anywhere, but in the interest + // of maintaining backward compatibility in case you use it, it's deprecated. + .nav-divider { + @include nav-divider(); + } + + // Prevent IE8 from misplacing imgs + // + // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989 + > li > a > img { + max-width: none; + } +} + + +// Tabs +// ------------------------- + +// Give the tabs something to sit on +.nav-tabs { + border-bottom: 1px solid $nav-tabs-border-color; + > li { + float: left; + // Make the list-items overlay the bottom border + margin-bottom: -1px; + + // Actual tabs (as links) + > a { + margin-right: 2px; + line-height: $line-height-base; + border: 1px solid transparent; + border-radius: $border-radius-base $border-radius-base 0 0; + &:hover { + border-color: $nav-tabs-link-hover-border-color $nav-tabs-link-hover-border-color $nav-tabs-border-color; + } + } + + // Active state, and its :hover to override normal :hover + &.active > a { + &, + &:hover, + &:focus { + color: $nav-tabs-active-link-hover-color; + background-color: $nav-tabs-active-link-hover-bg; + border: 1px solid $nav-tabs-active-link-hover-border-color; + border-bottom-color: transparent; + cursor: default; + } + } + } + // pulling this in mainly for less shorthand + &.nav-justified { + @extend .nav-justified; + @extend .nav-tabs-justified; + } +} + + +// Pills +// ------------------------- +.nav-pills { + > li { + float: left; + + // Links rendered as pills + > a { + border-radius: $nav-pills-border-radius; + } + + // Active state + &.active > a { + &, + &:hover, + &:focus { + color: $nav-pills-active-link-hover-color; + background-color: $nav-pills-active-link-hover-bg; + } + } + } +} + + +// Stacked pills +.nav-stacked { + > li { + float: none; + + li { + margin-top: 2px; + margin-left: 0; // no need for this gap between nav items + } + } +} + + +// Nav variations +// -------------------------------------------------- + +// Justified nav links +// ------------------------- + +.nav-justified { + width: 100%; + + > li { + float: none; + > a { + text-align: left; + margin-bottom: 5px; + } + } + + > .dropdown .dropdown-menu { + top: auto; + left: auto; + } + + @media (min-width: $screen-sm-min) { + > li { + display: table-cell; + width: 1%; + > a { + margin-bottom: 0; + } + } + } +} + +// Move borders to anchors instead of bottom of list +// +// Mixin for adding on top the shared `.nav-justified` styles for our tabs +.nav-tabs-justified { + border-bottom: 0; + + > li > a { + // Override margin from .nav-tabs + margin-right: 0; + border-radius: $border-radius-base; + } + + > .active > a, + > .active > a:hover, + > .active > a:focus { + border: 1px solid $nav-tabs-justified-link-border-color; + } + + @media (min-width: $screen-sm-min) { + > li > a { + border-bottom: 1px solid $nav-tabs-justified-link-border-color; + border-radius: $border-radius-base $border-radius-base 0 0; + } + > .active > a, + > .active > a:hover, + > .active > a:focus { + border-bottom-color: $nav-tabs-justified-active-link-border-color; + } + } +} + + +// Tabbable tabs +// ------------------------- + +// Hide tabbable panes to start, show them when `.active` +.tab-content { + > .tab-pane { + display: none; + } + > .active { + display: block; + } +} + + +// Dropdowns +// ------------------------- + +// Specific dropdowns +.nav-tabs .dropdown-menu { + // make dropdown border overlap tab border + margin-top: -1px; + // Remove the top rounded corners here since there is a hard edge above the menu + @include border-top-radius(0); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_normalize.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_normalize.scss new file mode 100644 index 000000000..ce04b6a2f --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_normalize.scss @@ -0,0 +1,425 @@ +/*! normalize.css v3.0.1 | MIT License | git.io/normalize */ + +// +// 1. Set default font family to sans-serif. +// 2. Prevent iOS text size adjust after orientation change, without disabling +// user zoom. +// + +html { + font-family: sans-serif; // 1 + -ms-text-size-adjust: 100%; // 2 + -webkit-text-size-adjust: 100%; // 2 +} + +// +// Remove default margin. +// + +body { + margin: 0; +} + +// HTML5 display definitions +// ========================================================================== + +// +// Correct `block` display not defined for any HTML5 element in IE 8/9. +// Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox. +// Correct `block` display not defined for `main` in IE 11. +// + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +// +// 1. Correct `inline-block` display not defined in IE 8/9. +// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. +// + +audio, +canvas, +progress, +video { + display: inline-block; // 1 + vertical-align: baseline; // 2 +} + +// +// Prevent modern browsers from displaying `audio` without controls. +// Remove excess height in iOS 5 devices. +// + +audio:not([controls]) { + display: none; + height: 0; +} + +// +// Address `[hidden]` styling not present in IE 8/9/10. +// Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. +// + +[hidden], +template { + display: none; +} + +// Links +// ========================================================================== + +// +// Remove the gray background color from active links in IE 10. +// + +a { + background: transparent; +} + +// +// Improve readability when focused and also mouse hovered in all browsers. +// + +a:active, +a:hover { + outline: 0; +} + +// Text-level semantics +// ========================================================================== + +// +// Address styling not present in IE 8/9/10/11, Safari, and Chrome. +// + +abbr[title] { + border-bottom: 1px dotted; +} + +// +// Address style set to `bolder` in Firefox 4+, Safari, and Chrome. +// + +b, +strong { + font-weight: bold; +} + +// +// Address styling not present in Safari and Chrome. +// + +dfn { + font-style: italic; +} + +// +// Address variable `h1` font-size and margin within `section` and `article` +// contexts in Firefox 4+, Safari, and Chrome. +// + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +// +// Address styling not present in IE 8/9. +// + +mark { + background: #ff0; + color: #000; +} + +// +// Address inconsistent and variable font size in all browsers. +// + +small { + font-size: 80%; +} + +// +// Prevent `sub` and `sup` affecting `line-height` in all browsers. +// + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +// Embedded content +// ========================================================================== + +// +// Remove border when inside `a` element in IE 8/9/10. +// + +img { + border: 0; +} + +// +// Correct overflow not hidden in IE 9/10/11. +// + +svg:not(:root) { + overflow: hidden; +} + +// Grouping content +// ========================================================================== + +// +// Address margin not present in IE 8/9 and Safari. +// + +figure { + margin: 1em 40px; +} + +// +// Address differences between Firefox and other browsers. +// + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +// +// Contain overflow in all browsers. +// + +pre { + overflow: auto; +} + +// +// Address odd `em`-unit font size rendering in all browsers. +// + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +// Forms +// ========================================================================== + +// +// Known limitation: by default, Chrome and Safari on OS X allow very limited +// styling of `select`, unless a `border` property is set. +// + +// +// 1. Correct color not being inherited. +// Known issue: affects color of disabled elements. +// 2. Correct font properties not being inherited. +// 3. Address margins set differently in Firefox 4+, Safari, and Chrome. +// + +button, +input, +optgroup, +select, +textarea { + color: inherit; // 1 + font: inherit; // 2 + margin: 0; // 3 +} + +// +// Address `overflow` set to `hidden` in IE 8/9/10/11. +// + +button { + overflow: visible; +} + +// +// Address inconsistent `text-transform` inheritance for `button` and `select`. +// All other form control elements do not inherit `text-transform` values. +// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. +// Correct `select` style inheritance in Firefox. +// + +button, +select { + text-transform: none; +} + +// +// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` +// and `video` controls. +// 2. Correct inability to style clickable `input` types in iOS. +// 3. Improve usability and consistency of cursor style between image-type +// `input` and others. +// + +button, +html input[type="button"], // 1 +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; // 2 + cursor: pointer; // 3 +} + +// +// Re-set default cursor for disabled elements. +// + +button[disabled], +html input[disabled] { + cursor: default; +} + +// +// Remove inner padding and border in Firefox 4+. +// + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +// +// Address Firefox 4+ setting `line-height` on `input` using `!important` in +// the UA stylesheet. +// + +input { + line-height: normal; +} + +// +// It's recommended that you don't attempt to style these elements. +// Firefox's implementation doesn't respect box-sizing, padding, or width. +// +// 1. Address box sizing set to `content-box` in IE 8/9/10. +// 2. Remove excess padding in IE 8/9/10. +// + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; // 1 + padding: 0; // 2 +} + +// +// Fix the cursor style for Chrome's increment/decrement buttons. For certain +// `font-size` values of the `input`, it causes the cursor style of the +// decrement button to change from `default` to `text`. +// + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +// +// 1. Address `appearance` set to `searchfield` in Safari and Chrome. +// 2. Address `box-sizing` set to `border-box` in Safari and Chrome +// (include `-moz` to future-proof). +// + +input[type="search"] { + -webkit-appearance: textfield; // 1 + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; // 2 + box-sizing: content-box; +} + +// +// Remove inner padding and search cancel button in Safari and Chrome on OS X. +// Safari (but not Chrome) clips the cancel button when the search input has +// padding (and `textfield` appearance). +// + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +// +// Define consistent border, margin, and padding. +// + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +// +// 1. Correct `color` not being inherited in IE 8/9/10/11. +// 2. Remove padding so people aren't caught out if they zero out fieldsets. +// + +legend { + border: 0; // 1 + padding: 0; // 2 +} + +// +// Remove default vertical scrollbar in IE 8/9/10/11. +// + +textarea { + overflow: auto; +} + +// +// Don't inherit the `font-weight` (applied by a rule above). +// NOTE: the default cannot safely be changed in Chrome and Safari on OS X. +// + +optgroup { + font-weight: bold; +} + +// Tables +// ========================================================================== + +// +// Remove most spacing between table cells. +// + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_pager.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_pager.scss new file mode 100644 index 000000000..6531fe6f8 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_pager.scss @@ -0,0 +1,55 @@ +// +// Pager pagination +// -------------------------------------------------- + + +.pager { + padding-left: 0; + margin: $line-height-computed 0; + list-style: none; + text-align: center; + @include clearfix(); + li { + display: inline; + > a, + > span { + display: inline-block; + padding: 5px 14px; + background-color: $pager-bg; + border: 1px solid $pager-border; + border-radius: $pager-border-radius; + } + + > a:hover, + > a:focus { + text-decoration: none; + background-color: $pager-hover-bg; + } + } + + .next { + > a, + > span { + float: right; + } + } + + .previous { + > a, + > span { + float: left; + } + } + + .disabled { + > a, + > a:hover, + > a:focus, + > span { + color: $pager-disabled-color; + background-color: $pager-bg; + cursor: not-allowed; + } + } + +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_pagination.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_pagination.scss new file mode 100644 index 000000000..44c12226b --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_pagination.scss @@ -0,0 +1,88 @@ +// +// Pagination (multiple pages) +// -------------------------------------------------- +.pagination { + display: inline-block; + padding-left: 0; + margin: $line-height-computed 0; + border-radius: $border-radius-base; + + > li { + display: inline; // Remove list-style and block-level defaults + > a, + > span { + position: relative; + float: left; // Collapse white-space + padding: $padding-base-vertical $padding-base-horizontal; + line-height: $line-height-base; + text-decoration: none; + color: $pagination-color; + background-color: $pagination-bg; + border: 1px solid $pagination-border; + margin-left: -1px; + } + &:first-child { + > a, + > span { + margin-left: 0; + @include border-left-radius($border-radius-base); + } + } + &:last-child { + > a, + > span { + @include border-right-radius($border-radius-base); + } + } + } + + > li > a, + > li > span { + &:hover, + &:focus { + color: $pagination-hover-color; + background-color: $pagination-hover-bg; + border-color: $pagination-hover-border; + } + } + + > .active > a, + > .active > span { + &, + &:hover, + &:focus { + z-index: 2; + color: $pagination-active-color; + background-color: $pagination-active-bg; + border-color: $pagination-active-border; + cursor: default; + } + } + + > .disabled { + > span, + > span:hover, + > span:focus, + > a, + > a:hover, + > a:focus { + color: $pagination-disabled-color; + background-color: $pagination-disabled-bg; + border-color: $pagination-disabled-border; + cursor: not-allowed; + } + } +} + +// Sizing +// -------------------------------------------------- + +// Large +.pagination-lg { + @include pagination-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $border-radius-large); +} + +// Small +.pagination-sm { + @include pagination-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $border-radius-small); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_panels.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_panels.scss new file mode 100644 index 000000000..57cdcbc1f --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_panels.scss @@ -0,0 +1,243 @@ +// +// Panels +// -------------------------------------------------- + + +// Base class +.panel { + margin-bottom: $line-height-computed; + background-color: $panel-bg; + border: 1px solid transparent; + border-radius: $panel-border-radius; + @include box-shadow(0 1px 1px rgba(0,0,0,.05)); +} + +// Panel contents +.panel-body { + padding: $panel-body-padding; + @include clearfix(); +} + +// Optional heading +.panel-heading { + padding: $panel-heading-padding; + border-bottom: 1px solid transparent; + @include border-top-radius(($panel-border-radius - 1)); + + > .dropdown .dropdown-toggle { + color: inherit; + } +} + +// Within heading, strip any `h*` tag of its default margins for spacing. +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: ceil(($font-size-base * 1.125)); + color: inherit; + + > a { + color: inherit; + } +} + +// Optional footer (stays gray in every modifier class) +.panel-footer { + padding: $panel-footer-padding; + background-color: $panel-footer-bg; + border-top: 1px solid $panel-inner-border; + @include border-bottom-radius(($panel-border-radius - 1)); +} + + +// List groups in panels +// +// By default, space out list group content from panel headings to account for +// any kind of custom content between the two. + +.panel { + > .list-group { + margin-bottom: 0; + + .list-group-item { + border-width: 1px 0; + border-radius: 0; + } + + // Add border top radius for first one + &:first-child { + .list-group-item:first-child { + border-top: 0; + @include border-top-radius(($panel-border-radius - 1)); + } + } + // Add border bottom radius for last one + &:last-child { + .list-group-item:last-child { + border-bottom: 0; + @include border-bottom-radius(($panel-border-radius - 1)); + } + } + } +} +// Collapse space between when there's no additional content. +.panel-heading + .list-group { + .list-group-item:first-child { + border-top-width: 0; + } +} +.list-group + .panel-footer { + border-top-width: 0; +} + +// Tables in panels +// +// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and +// watch it go full width. + +.panel { + > .table, + > .table-responsive > .table, + > .panel-collapse > .table { + margin-bottom: 0; + } + // Add border top radius for first one + > .table:first-child, + > .table-responsive:first-child > .table:first-child { + @include border-top-radius(($panel-border-radius - 1)); + + > thead:first-child, + > tbody:first-child { + > tr:first-child { + td:first-child, + th:first-child { + border-top-left-radius: ($panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-top-right-radius: ($panel-border-radius - 1); + } + } + } + } + // Add border bottom radius for last one + > .table:last-child, + > .table-responsive:last-child > .table:last-child { + @include border-bottom-radius(($panel-border-radius - 1)); + + > tbody:last-child, + > tfoot:last-child { + > tr:last-child { + td:first-child, + th:first-child { + border-bottom-left-radius: ($panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-bottom-right-radius: ($panel-border-radius - 1); + } + } + } + } + > .panel-body + .table, + > .panel-body + .table-responsive { + border-top: 1px solid $table-border-color; + } + > .table > tbody:first-child > tr:first-child th, + > .table > tbody:first-child > tr:first-child td { + border-top: 0; + } + > .table-bordered, + > .table-responsive > .table-bordered { + border: 0; + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + > thead, + > tbody { + > tr:first-child { + > td, + > th { + border-bottom: 0; + } + } + } + > tbody, + > tfoot { + > tr:last-child { + > td, + > th { + border-bottom: 0; + } + } + } + } + > .table-responsive { + border: 0; + margin-bottom: 0; + } +} + + +// Collapsable panels (aka, accordion) +// +// Wrap a series of panels in `.panel-group` to turn them into an accordion with +// the help of our collapse JavaScript plugin. + +.panel-group { + margin-bottom: $line-height-computed; + + // Tighten up margin so it's only between panels + .panel { + margin-bottom: 0; + border-radius: $panel-border-radius; + + .panel { + margin-top: 5px; + } + } + + .panel-heading { + border-bottom: 0; + + .panel-collapse > .panel-body { + border-top: 1px solid $panel-inner-border; + } + } + .panel-footer { + border-top: 0; + + .panel-collapse .panel-body { + border-bottom: 1px solid $panel-inner-border; + } + } +} + + +// Contextual variations +.panel-default { + @include panel-variant($panel-default-border, $panel-default-text, $panel-default-heading-bg, $panel-default-border); +} +.panel-primary { + @include panel-variant($panel-primary-border, $panel-primary-text, $panel-primary-heading-bg, $panel-primary-border); +} +.panel-success { + @include panel-variant($panel-success-border, $panel-success-text, $panel-success-heading-bg, $panel-success-border); +} +.panel-info { + @include panel-variant($panel-info-border, $panel-info-text, $panel-info-heading-bg, $panel-info-border); +} +.panel-warning { + @include panel-variant($panel-warning-border, $panel-warning-text, $panel-warning-heading-bg, $panel-warning-border); +} +.panel-danger { + @include panel-variant($panel-danger-border, $panel-danger-text, $panel-danger-heading-bg, $panel-danger-border); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_popovers.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_popovers.scss new file mode 100644 index 000000000..1cf27aed5 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_popovers.scss @@ -0,0 +1,133 @@ +// +// Popovers +// -------------------------------------------------- + + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: $zindex-popover; + display: none; + max-width: $popover-max-width; + padding: 1px; + text-align: left; // Reset given new insertion method + background-color: $popover-bg; + background-clip: padding-box; + border: 1px solid $popover-fallback-border-color; + border: 1px solid $popover-border-color; + border-radius: $border-radius-large; + @include box-shadow(0 5px 10px rgba(0,0,0,.2)); + + // Overrides for proper insertion + white-space: normal; + + // Offset the popover to account for the popover arrow + &.top { margin-top: -$popover-arrow-width; } + &.right { margin-left: $popover-arrow-width; } + &.bottom { margin-top: $popover-arrow-width; } + &.left { margin-left: -$popover-arrow-width; } +} + +.popover-title { + margin: 0; // reset heading margin + padding: 8px 14px; + font-size: $font-size-base; + font-weight: normal; + line-height: 18px; + background-color: $popover-title-bg; + border-bottom: 1px solid darken($popover-title-bg, 5%); + border-radius: ($border-radius-large - 1) ($border-radius-large - 1) 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +// Arrows +// +// .arrow is outer, .arrow:after is inner + +.popover > .arrow { + &, + &:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + } +} +.popover > .arrow { + border-width: $popover-arrow-outer-width; +} +.popover > .arrow:after { + border-width: $popover-arrow-width; + content: ""; +} + +.popover { + &.top > .arrow { + left: 50%; + margin-left: -$popover-arrow-outer-width; + border-bottom-width: 0; + border-top-color: $popover-arrow-outer-fallback-color; // IE8 fallback + border-top-color: $popover-arrow-outer-color; + bottom: -$popover-arrow-outer-width; + &:after { + content: " "; + bottom: 1px; + margin-left: -$popover-arrow-width; + border-bottom-width: 0; + border-top-color: $popover-arrow-color; + } + } + &.right > .arrow { + top: 50%; + left: -$popover-arrow-outer-width; + margin-top: -$popover-arrow-outer-width; + border-left-width: 0; + border-right-color: $popover-arrow-outer-fallback-color; // IE8 fallback + border-right-color: $popover-arrow-outer-color; + &:after { + content: " "; + left: 1px; + bottom: -$popover-arrow-width; + border-left-width: 0; + border-right-color: $popover-arrow-color; + } + } + &.bottom > .arrow { + left: 50%; + margin-left: -$popover-arrow-outer-width; + border-top-width: 0; + border-bottom-color: $popover-arrow-outer-fallback-color; // IE8 fallback + border-bottom-color: $popover-arrow-outer-color; + top: -$popover-arrow-outer-width; + &:after { + content: " "; + top: 1px; + margin-left: -$popover-arrow-width; + border-top-width: 0; + border-bottom-color: $popover-arrow-color; + } + } + + &.left > .arrow { + top: 50%; + right: -$popover-arrow-outer-width; + margin-top: -$popover-arrow-outer-width; + border-right-width: 0; + border-left-color: $popover-arrow-outer-fallback-color; // IE8 fallback + border-left-color: $popover-arrow-outer-color; + &:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: $popover-arrow-color; + bottom: -$popover-arrow-width; + } + } + +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_print.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_print.scss new file mode 100644 index 000000000..3655d0395 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_print.scss @@ -0,0 +1,101 @@ +// +// Basic print styles +// -------------------------------------------------- +// Source: https://github.com/h5bp/html5-boilerplate/blob/master/css/main.css + +@media print { + + * { + text-shadow: none !important; + color: #000 !important; // Black prints faster: h5bp.com/s + background: transparent !important; + box-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + // Don't show links for images, or javascript/internal links + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; // h5bp.com/t + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } + + // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245 + // Once fixed, we can just straight up remove this. + select { + background: #fff !important; + } + + // Bootstrap components + .navbar { + display: none; + } + .table { + td, + th { + background-color: #fff !important; + } + } + .btn, + .dropup > .btn { + > .caret { + border-top-color: #000 !important; + } + } + .label { + border: 1px solid #000; + } + + .table { + border-collapse: collapse !important; + } + .table-bordered { + th, + td { + border: 1px solid #ddd !important; + } + } + +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_progress-bars.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_progress-bars.scss new file mode 100644 index 000000000..a65679577 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_progress-bars.scss @@ -0,0 +1,107 @@ +// +// Progress bars +// -------------------------------------------------- + + +// Bar animations +// ------------------------- + +// WebKit +@-webkit-keyframes progress-bar-stripes { + from { background-position: 40px 0; } + to { background-position: 0 0; } +} + +// Spec and IE10+ +@keyframes progress-bar-stripes { + from { background-position: 40px 0; } + to { background-position: 0 0; } +} + + + +// Bar itself +// ------------------------- + +// Outer container +.progress { + overflow: hidden; + height: $line-height-computed; + background-color: $progress-bg; + border-radius: $border-radius-base; + position: relative; + @include box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); +} + +// Bar of progress +.progress-bar { + float: left; + width: 0%; + height: 100%; + font-size: $font-size-small; + line-height: $line-height-computed; + color: $progress-bar-color; + text-align: center; + background-color: $progress-bar-bg; + position: relative; + z-index: 2; + @include box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); + @include transition(width .6s ease); +} + +// Striped bars +// +// `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the +// `.progress-bar-striped` class, which you just add to an existing +// `.progress-bar`. +.progress-striped .progress-bar, +.progress-bar-striped { + @include gradient-striped(); + background-size: 40px 40px; +} + +// Call animation for the active one +// +// `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the +// `.progress-bar.active` approach. +.progress.active .progress-bar, +.progress-bar.active { + @include animation(progress-bar-stripes 2s linear infinite); +} + +// Account for lower percentages +.progress-bar { + &[aria-valuenow="1"], + &[aria-valuenow="2"] { + min-width: 30px; + } + + &[aria-valuenow="0"] { + color: $gray-light; + min-width: 30px; + background-color: transparent; + background-image: none; + box-shadow: none; + } +} + + + +// Variations +// ------------------------- + +.progress-bar-success { + @include progress-bar-variant($progress-bar-success-bg); +} + +.progress-bar-info { + @include progress-bar-variant($progress-bar-info-bg); +} + +.progress-bar-warning { + @include progress-bar-variant($progress-bar-warning-bg); +} + +.progress-bar-danger { + @include progress-bar-variant($progress-bar-danger-bg); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_responsive-embed.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_responsive-embed.scss new file mode 100644 index 000000000..a884d49fe --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_responsive-embed.scss @@ -0,0 +1,34 @@ +// Embeds responsive +// +// Credit: Nicolas Gallagher and SUIT CSS. + +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; + + .embed-responsive-item, + iframe, + embed, + object { + position: absolute; + top: 0; + left: 0; + bottom: 0; + height: 100%; + width: 100%; + border: 0; + } + + // Modifier class for 16:9 aspect ratio + &.embed-responsive-16by9 { + padding-bottom: 56.25%; + } + + // Modifier class for 4:3 aspect ratio + &.embed-responsive-4by3 { + padding-bottom: 75%; + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_responsive-utilities.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_responsive-utilities.scss new file mode 100644 index 000000000..7cd861bf1 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_responsive-utilities.scss @@ -0,0 +1,180 @@ +// +// Responsive: Utility classes +// -------------------------------------------------- + + +// IE10 in Windows (Phone) 8 +// +// Support for responsive views via media queries is kind of borked in IE10, for +// Surface/desktop in split view and for Windows Phone 8. This particular fix +// must be accompanied by a snippet of JavaScript to sniff the user agent and +// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at +// our Getting Started page for more information on this bug. +// +// For more information, see the following: +// +// Issue: https://github.com/twbs/bootstrap/issues/10497 +// Docs: http://getbootstrap.com/getting-started/#support-ie10-width +// Source: http://timkadlec.com/2013/01/windows-phone-8-and-device-width/ +// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/ + +@-ms-viewport { + width: device-width; +} + + +// Visibility utilities +// Note: Deprecated .visible-xs, .visible-sm, .visible-md, and .visible-lg as of v3.2.0 + +@include responsive-invisibility('.visible-xs, .visible-sm, .visible-md, .visible-lg'); + +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} + +@media (max-width: $screen-xs-max) { + @include responsive-visibility('.visible-xs'); +} +.visible-xs-block { + @media (max-width: $screen-xs-max) { + display: block !important; + } +} +.visible-xs-inline { + @media (max-width: $screen-xs-max) { + display: inline !important; + } +} +.visible-xs-inline-block { + @media (max-width: $screen-xs-max) { + display: inline-block !important; + } +} + +@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include responsive-visibility('.visible-sm'); +} +.visible-sm-block { + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + display: block !important; + } +} +.visible-sm-inline { + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + display: inline !important; + } +} +.visible-sm-inline-block { + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + display: inline-block !important; + } +} + +@media (min-width: $screen-md-min) and (max-width: $screen-md-max) { + @include responsive-visibility('.visible-md'); +} +.visible-md-block { + @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { + display: block !important; + } +} +.visible-md-inline { + @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { + display: inline !important; + } +} +.visible-md-inline-block { + @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { + display: inline-block !important; + } +} + +@media (min-width: $screen-lg-min) { + @include responsive-visibility('.visible-lg'); +} +.visible-lg-block { + @media (min-width: $screen-lg-min) { + display: block !important; + } +} +.visible-lg-inline { + @media (min-width: $screen-lg-min) { + display: inline !important; + } +} +.visible-lg-inline-block { + @media (min-width: $screen-lg-min) { + display: inline-block !important; + } +} + +@media (max-width: $screen-xs-max) { + @include responsive-invisibility('.hidden-xs'); +} + +@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include responsive-invisibility('.hidden-sm'); +} + +@media (max-width: 768px), (max-height: 669px) { + .toggle-sidebar { + display: none !important; + } +} + +@media (min-width: $screen-md-min) and (max-width: $screen-md-max) { + @include responsive-invisibility('.hidden-md'); +} + +@media (min-width: $screen-lg-min) { + @include responsive-invisibility('.hidden-lg'); +} + + +// Print utilities +// +// Media queries are placed on the inside to be mixin-friendly. + +// Note: Deprecated .visible-print as of v3.2.0 + +@include responsive-invisibility('.visible-print'); + +@media print { + @include responsive-visibility('.visible-print'); +} +.visible-print-block { + display: none !important; + + @media print { + display: block !important; + } +} +.visible-print-inline { + display: none !important; + + @media print { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; + + @media print { + display: inline-block !important; + } +} + +@media print { + @include responsive-invisibility('.hidden-print'); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_scaffolding.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_scaffolding.scss new file mode 100644 index 000000000..8cea4cdc3 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_scaffolding.scss @@ -0,0 +1,150 @@ +// +// Scaffolding +// -------------------------------------------------- + + +// Reset the box-sizing +// +// Heads up! This reset may cause conflicts with some third-party widgets. +// For recommendations on resolving such conflicts, see +// http://getbootstrap.com/getting-started/#third-box-sizing +* { + @include box-sizing(border-box); +} +*:before, +*:after { + @include box-sizing(border-box); +} + + +// Body reset + +html { + font-size: 10px; + -webkit-tap-highlight-color: rgba(0,0,0,0); +} + +body { + font-family: $font-family-base; + font-size: $font-size-base; + line-height: $line-height-base; + color: $text-color; + background-color: $body-bg; +} + +// Reset fonts for relevant elements +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + + +// Links + +a { + color: $link-color; + text-decoration: none; + + &:hover, + &:focus { + color: $link-hover-color; + text-decoration: underline; + } + + &:focus { + @include tab-focus(); + } +} + + +// Figures +// +// We reset this here because previously Normalize had no `figure` margins. This +// ensures we don't break anyone's use of the element. + +figure { + margin: 0; +} + + +// Images + +img { + vertical-align: middle; +} + +// Responsive images (ensure images don't scale beyond their parents) +.img-responsive { + @include img-responsive(); +} + +// Rounded corners +.img-rounded { + border-radius: $border-radius-large; +} + +// Image thumbnails +// +// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`. +.img-thumbnail { + padding: $thumbnail-padding; + line-height: $line-height-base; + background-color: $thumbnail-bg; + border: 1px solid $thumbnail-border; + border-radius: $thumbnail-border-radius; + @include transition(all .2s ease-in-out); + + // Keep them at most 100% wide + @include img-responsive(inline-block); +} + +// Perfect circle +.img-circle { + border-radius: 50%; // set radius in percents +} + + +// Horizontal rules + +hr { + margin-top: $line-height-computed; + margin-bottom: $line-height-computed; + border: 0; + border-top: 1px solid $hr-border; +} + + +// Only display content to screen readers +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} + +// Use in conjunction with .sr-only to only display content when it's focused. +// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 +// Credit: HTML5 Boilerplate + +.sr-only-focusable { + &:active, + &:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_tables.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_tables.scss new file mode 100644 index 000000000..629cab749 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_tables.scss @@ -0,0 +1,236 @@ +// +// Tables +// -------------------------------------------------- + + +table { + background-color: $table-bg; + color: #444; +} +th { + text-align: left; +} + + +// Baseline styles + +.table { + width: 100%; + max-width: 100%; + margin-bottom: $line-height-computed; + // Cells + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: $table-cell-padding; + line-height: $line-height-base; + vertical-align: top; + border-top: 1px solid $table-border-color; + } + } + } + // Bottom align for column headings + > thead > tr > th { + vertical-align: bottom; + border-bottom: 1px solid $table-border-color; + } + // Remove top border from thead by default + > caption + thead, + > colgroup + thead, + > thead:first-child { + > tr:first-child { + > th, + > td { + border-top: 0; + font-family: 'SourceSansProSemibold'; + font-weight: normal; + } + } + } + // Account for multiple tbody instances + > tbody + tbody { + border-top: 2px solid $table-border-color; + } + + // Nesting + .table { + background-color: $body-bg; + } +} + + +// Condensed table w/ half padding + +.table-condensed { + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: $table-condensed-cell-padding; + } + } + } +} + + +// Bordered version +// +// Add borders all around the table and between all the columns. + +.table-bordered { + border: 1px solid $table-border-color; + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + border: 1px solid $table-border-color; + } + } + } + > thead > tr { + > th, + > td { + border-bottom-width: 2px; + } + } +} + + +// Zebra-striping +// +// Default zebra-stripe styles (alternating gray and transparent backgrounds) + +.table-striped { + > tbody > tr:nth-child(odd) { + > td, + > th { + background-color: $table-bg-accent; + } + } +} + + +// Hover effect +// +// Placed here since it has to come after the potential zebra striping + +.table-hover { + > tbody > tr:hover { + > td, + > th { + background-color: $table-bg-hover; + } + } +} + + +// Table cell sizing +// +// Reset default table behavior + +table col[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-column; +} +table { + td, + th { + &[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-cell; + } + } +} + + +// Table backgrounds +// +// Exact selectors below required to override `.table-striped` and prevent +// inheritance to nested tables. + +// Generate the contextual variants +@include table-row-variant('active', $table-bg-active); +@include table-row-variant('success', $state-success-bg); +@include table-row-variant('info', $state-info-bg); +@include table-row-variant('warning', $state-warning-bg); +@include table-row-variant('danger', $state-danger-bg); + + +// Responsive tables +// +// Wrap your tables in `.table-responsive` and we'll make them mobile friendly +// by enabling horizontal scrolling. Only applies <768px. Everything above that +// will display normally. + +.table-responsive { + @media screen and (max-width: $screen-xs-max) { + width: 100%; + margin-bottom: ($line-height-computed * 0.75); + overflow-y: hidden; + overflow-x: auto; + -ms-overflow-style: -ms-autohiding-scrollbar; +/* border: 1px solid $table-border-color; */ + -webkit-overflow-scrolling: touch; + + // Tighten up spacing + > .table { + margin-bottom: 0; + + // Ensure the content doesn't wrap + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + white-space: nowrap; + } + } + } + } + + // Special overrides for the bordered tables + > .table-bordered { + border: 0; + + // Nuke the appropriate borders so that the parent can handle them + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + + // Only nuke the last row's bottom-border in `tbody` and `tfoot` since + // chances are there will be only one `tr` in a `thead` and that would + // remove the border altogether. + > tbody, + > tfoot { + > tr:last-child { + > th, + > td { + border-bottom: 0; + } + } + } + + } + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_theme.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_theme.scss new file mode 100644 index 000000000..00386a288 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_theme.scss @@ -0,0 +1,258 @@ + +// +// Load core variables and mixins +// -------------------------------------------------- + +@import "variables"; +@import "mixins"; + + + +// +// Buttons +// -------------------------------------------------- + +// Common styles +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0,0,0,.2); + $shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075); + @include box-shadow($shadow); + + // Reset the shadow + &:active, + &.active { + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } +} + +// Mixin for generating new styles +@mixin btn-styles($btn-color: #555) { + @include gradient-vertical($start-color: $btn-color, $end-color: darken($btn-color, 12%)); + @include reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners + background-repeat: repeat-x; + border-color: darken($btn-color, 14%); + + &:hover, + &:focus { + background-color: darken($btn-color, 12%); + background-position: 0 -15px; + } + + &:active, + &.active { + background-color: darken($btn-color, 12%); + border-color: darken($btn-color, 14%); + } + + &:disabled, + &[disabled] { + background-color: darken($btn-color, 12%); + background-image: none; + } +} + +// Common styles +.btn { + // Remove the gradient for the pressed/active state + &:active, + &.active { + background-image: none; + } +} + +// Apply the mixin to the buttons +.btn-default { @include btn-styles($btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; } +.btn-primary { @include btn-styles($btn-primary-bg); } +.btn-success { @include btn-styles($btn-success-bg); } +.btn-info { @include btn-styles($btn-info-bg); } +.btn-warning { @include btn-styles($btn-warning-bg); } +.btn-danger { @include btn-styles($btn-danger-bg); } + + + +// +// Images +// -------------------------------------------------- + +.thumbnail, +.img-thumbnail { + @include box-shadow(0 1px 2px rgba(0,0,0,.075)); +} + + + +// +// Dropdowns +// -------------------------------------------------- + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + @include gradient-vertical($start-color: $dropdown-link-hover-bg, $end-color: darken($dropdown-link-hover-bg, 5%)); + background-color: darken($dropdown-link-hover-bg, 5%); +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + @include gradient-vertical($start-color: $dropdown-link-active-bg, $end-color: darken($dropdown-link-active-bg, 5%)); + background-color: darken($dropdown-link-active-bg, 5%); +} + + + +// +// Navbar +// -------------------------------------------------- + +// Default navbar +.navbar-default { + @include gradient-vertical($start-color: lighten($navbar-default-bg, 10%), $end-color: $navbar-default-bg); + @include reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered + border-radius: $navbar-border-radius; + $shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075); + @include box-shadow($shadow); + + .navbar-nav > .active > a { + @include gradient-vertical($start-color: darken($navbar-default-bg, 5%), $end-color: darken($navbar-default-bg, 2%)); + @include box-shadow(inset 0 3px 9px rgba(0,0,0,.075)); + } +} +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255,255,255,.25); +} + +// Inverted navbar +.navbar-inverse { + @include gradient-vertical($start-color: lighten($navbar-inverse-bg, 10%), $end-color: $navbar-inverse-bg); + @include reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered + + .navbar-nav > .active > a { + @include gradient-vertical($start-color: $navbar-inverse-bg, $end-color: lighten($navbar-inverse-bg, 2.5%)); + @include box-shadow(inset 0 3px 9px rgba(0,0,0,.25)); + } + + .navbar-brand, + .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + } +} + +// Undo rounded corners in static and fixed navbars +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} + + + +// +// Alerts +// -------------------------------------------------- + +// Common styles +.alert { + text-shadow: 0 1px 0 rgba(255,255,255,.2); + $shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05); + @include box-shadow($shadow); +} + +// Mixin for generating new styles +@mixin alert-styles($color) { + @include gradient-vertical($start-color: $color, $end-color: darken($color, 7.5%)); + border-color: darken($color, 15%); +} + +// Apply the mixin to the alerts +.alert-success { @include alert-styles($alert-success-bg); } +.alert-info { @include alert-styles($alert-info-bg); } +.alert-warning { @include alert-styles($alert-warning-bg); } +.alert-danger { @include alert-styles($alert-danger-bg); } + + + +// +// Progress bars +// -------------------------------------------------- + +// Give the progress background some depth +.progress { + @include gradient-vertical($start-color: darken($progress-bg, 4%), $end-color: $progress-bg) +} + +// Mixin for generating new styles +@mixin progress-bar-styles($color) { + @include gradient-vertical($start-color: $color, $end-color: darken($color, 10%)); +} + +// Apply the mixin to the progress bars +.progress-bar { @include progress-bar-styles($progress-bar-bg); } +.progress-bar-success { @include progress-bar-styles($progress-bar-success-bg); } +.progress-bar-info { @include progress-bar-styles($progress-bar-info-bg); } +.progress-bar-warning { @include progress-bar-styles($progress-bar-warning-bg); } +.progress-bar-danger { @include progress-bar-styles($progress-bar-danger-bg); } + +// Reset the striped class because our mixins don't do multiple gradients and +// the above custom styles override the new `.progress-bar-striped` in v3.2.0. +.progress-bar-striped { + @include gradient-striped(); +} + + +// +// List groups +// -------------------------------------------------- + +.list-group { + border-radius: $border-radius-base; + @include box-shadow(0 1px 2px rgba(0,0,0,.075)); +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 darken($list-group-active-bg, 10%); + @include gradient-vertical($start-color: $list-group-active-bg, $end-color: darken($list-group-active-bg, 7.5%)); + border-color: darken($list-group-active-border, 7.5%); +} + + + +// +// Panels +// -------------------------------------------------- + +// Common styles +.panel { + @include box-shadow(0 1px 2px rgba(0,0,0,.05)); +} + +// Mixin for generating new styles +@mixin panel-heading-styles($color) { + @include gradient-vertical($start-color: $color, $end-color: darken($color, 5%)); +} + +// Apply the mixin to the panel headings only +.panel-default > .panel-heading { @include panel-heading-styles($panel-default-heading-bg); } +.panel-primary > .panel-heading { @include panel-heading-styles($panel-primary-heading-bg); } +.panel-success > .panel-heading { @include panel-heading-styles($panel-success-heading-bg); } +.panel-info > .panel-heading { @include panel-heading-styles($panel-info-heading-bg); } +.panel-warning > .panel-heading { @include panel-heading-styles($panel-warning-heading-bg); } +.panel-danger > .panel-heading { @include panel-heading-styles($panel-danger-heading-bg); } + + + +// +// Wells +// -------------------------------------------------- + +.well { + @include gradient-vertical($start-color: darken($well-bg, 5%), $end-color: $well-bg); + border-color: darken($well-bg, 10%); + $shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1); + @include box-shadow($shadow); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_thumbnails.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_thumbnails.scss new file mode 100644 index 000000000..3d5ed86d0 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_thumbnails.scss @@ -0,0 +1,38 @@ +// +// Thumbnails +// -------------------------------------------------- + + +// Mixin and adjust the regular image class +.thumbnail { + display: block; + padding: $thumbnail-padding; + margin-bottom: $line-height-computed; + line-height: $line-height-base; + background-color: $thumbnail-bg; + border: 1px solid $thumbnail-border; + border-radius: $thumbnail-border-radius; + @include transition(all .2s ease-in-out); + + > img, + a > img { + @include img-responsive(); + margin-left: auto; + margin-right: auto; + } + + // [converter] extracted a&:hover, a&:focus, a&.active to a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active + + // Image captions + .caption { + padding: $thumbnail-caption-padding; + color: $thumbnail-caption-color; + } +} + +// Add a hover state for linked versions only +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: $link-color; +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_tooltip.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_tooltip.scss new file mode 100644 index 000000000..dec674cb4 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_tooltip.scss @@ -0,0 +1,95 @@ +// +// Tooltips +// -------------------------------------------------- + + +// Base class +.tooltip { + position: absolute; + z-index: $zindex-tooltip; + display: block; + visibility: visible; + font-size: $font-size-small; + line-height: 1.4; + @include opacity(0); + + &.in { @include opacity($tooltip-opacity); } + &.top { margin-top: -3px; padding: $tooltip-arrow-width 0; } + &.right { margin-left: 3px; padding: 0 $tooltip-arrow-width; } + &.bottom { margin-top: 3px; padding: $tooltip-arrow-width 0; } + &.left { margin-left: -3px; padding: 0 $tooltip-arrow-width; } +} + +// Wrapper for the tooltip content +.tooltip-inner { + max-width: $tooltip-max-width; + padding: 3px 8px; + color: $tooltip-color; + text-align: center; + text-decoration: none; + background-color: $tooltip-bg; + border-radius: $border-radius-base; +} + +// Arrows +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip { + &.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -$tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width 0; + border-top-color: $tooltip-arrow-color; + } + &.top-left .tooltip-arrow { + bottom: 0; + left: $tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width 0; + border-top-color: $tooltip-arrow-color; + } + &.top-right .tooltip-arrow { + bottom: 0; + right: $tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width 0; + border-top-color: $tooltip-arrow-color; + } + &.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -$tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width $tooltip-arrow-width 0; + border-right-color: $tooltip-arrow-color; + } + &.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -$tooltip-arrow-width; + border-width: $tooltip-arrow-width 0 $tooltip-arrow-width $tooltip-arrow-width; + border-left-color: $tooltip-arrow-color; + } + &.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -$tooltip-arrow-width; + border-width: 0 $tooltip-arrow-width $tooltip-arrow-width; + border-bottom-color: $tooltip-arrow-color; + } + &.bottom-left .tooltip-arrow { + top: 0; + left: $tooltip-arrow-width; + border-width: 0 $tooltip-arrow-width $tooltip-arrow-width; + border-bottom-color: $tooltip-arrow-color; + } + &.bottom-right .tooltip-arrow { + top: 0; + right: $tooltip-arrow-width; + border-width: 0 $tooltip-arrow-width $tooltip-arrow-width; + border-bottom-color: $tooltip-arrow-color; + } +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_type.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_type.scss new file mode 100644 index 000000000..154e4fd04 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_type.scss @@ -0,0 +1,304 @@ +// +// Typography +// -------------------------------------------------- + + +// Headings +// ------------------------- + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + font-family: $headings-font-family; + font-weight: $headings-font-weight; + line-height: $headings-line-height; + color: $headings-color; + + small, + .small { + font-weight: normal; + line-height: 1; + color: $headings-small-color; + } +} + +h1, .h1, +h2, .h2, +h3, .h3 { + margin-top: $line-height-computed; + margin-bottom: calc($line-height-computed / 2); + + small, + .small { + font-size: 65%; + } +} +h4, .h4, +h5, .h5, +h6, .h6 { + margin-top: calc($line-height-computed / 2); + margin-bottom: calc($line-height-computed / 2); + + small, + .small { + font-size: 75%; + } +} + +h1, .h1 { font-size: $font-size-h1; } +h2, .h2 { font-size: $font-size-h2; } +h3, .h3 { font-size: $font-size-h3; } +h4, .h4 { font-size: $font-size-h4; } +h5, .h5 { font-size: $font-size-h5; } +h6, .h6 { font-size: $font-size-h6; } + + +// Body text +// ------------------------- + +p { + margin: 0 0 calc($line-height-computed / 2); +} + +.lead { + margin-bottom: $line-height-computed; + font-size: floor(calc($font-size-base * 1.15)); + font-weight: 300; + line-height: 1.4; + + @media (min-width: $screen-sm-min) { + font-size: calc($font-size-base * 1.5); + } +} + + +// Emphasis & misc +// ------------------------- + +// Ex: (12px small font / 14px base font) * 100% = about 85% +small, +.small { + font-size: floor(calc(100% * $font-size-small / $font-size-base)); +} + +// Undo browser default styling +cite { + font-style: normal; +} + +mark, +.mark { + background-color: $state-warning-bg; + padding: .2em; +} + +// Alignment +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.text-justify { text-align: justify; } +.text-nowrap { white-space: nowrap; } + +// Transformation +.text-lowercase { text-transform: lowercase; } +.text-uppercase { text-transform: uppercase; } +.text-capitalize { text-transform: capitalize; } + +// Contextual colors +.text-muted { + color: $text-muted; +} + +@include text-emphasis-variant('.text-primary', $brand-primary); + +@include text-emphasis-variant('.text-success', $state-success-text); + +@include text-emphasis-variant('.text-info', $state-info-text); + +@include text-emphasis-variant('.text-warning', $state-warning-text); + +@include text-emphasis-variant('.text-danger', $state-danger-text); + +// Contextual backgrounds +// For now we'll leave these alongside the text classes until v4 when we can +// safely shift things around (per SemVer rules). +.bg-primary { + // Given the contrast here, this is the only class to have its color inverted + // automatically. + color: #fff; +} +@include bg-variant('.bg-primary', $brand-primary); + +@include bg-variant('.bg-success', $state-success-bg); + +@include bg-variant('.bg-info', $state-info-bg); + +@include bg-variant('.bg-warning', $state-warning-bg); + +@include bg-variant('.bg-danger', $state-danger-bg); + + +// Page header +// ------------------------- + +.page-header { + padding-bottom: (calc($line-height-computed / 2) - 1); + margin: calc($line-height-computed * 2) 0 $line-height-computed; + border-bottom: 1px solid $page-header-border-color; +} + + +// Lists +// ------------------------- + +// Unordered and Ordered lists +ul, +ol { + margin-top: 0; + margin-bottom: calc($line-height-computed / 2); + ul, + ol { + margin-bottom: 0; + } +} + +// List options + +// Unstyled keeps list items block level, just removes default browser padding and list-style +.list-unstyled { + padding-left: 0; + list-style: none; +} + +// Inline turns list items into inline-block +.list-inline { + @extend .list-unstyled; + margin-left: -5px; + + > li { + float:left; + padding-left: 5px; + padding-right: 5px; + } +} + +// Description Lists +dl { + margin-top: 0; // Remove browser default + margin-bottom: $line-height-computed; +} +dt, +dd { + line-height: $line-height-base; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; // Undo browser default +} + +// Horizontal description lists +// +// Defaults to being stacked without any of the below styles applied, until the +// grid breakpoint is reached (default of ~768px). + +.dl-horizontal { + dd { + @include clearfix(); // Clear the floated `dt` if an empty `dd` is present + } + + @media (min-width: $grid-float-breakpoint) { + dt { + float: left; + width: ($dl-horizontal-offset - 20); + clear: left; + text-align: right; + @include text-overflow(); + } + dd { + margin-left: $dl-horizontal-offset; + } + } +} + + +// Misc +// ------------------------- + +// Abbreviations and acronyms +abbr[title], +// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted $abbr-border-color; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +// Blockquotes +blockquote { + padding: calc($line-height-computed / 2) $line-height-computed; + margin: 0 0 $line-height-computed; + font-size: $blockquote-font-size; + border-left: 5px solid $blockquote-border-color; + + p, + ul, + ol { + &:last-child { + margin-bottom: 0; + } + } + + // Note: Deprecated small and .small as of v3.1.0 + // Context: https://github.com/twbs/bootstrap/issues/11660 + footer, + small, + .small { + display: block; + font-size: 80%; // back to default font-size + line-height: $line-height-base; + color: $blockquote-small-color; + + &:before { + content: '\2014 \00A0'; // em dash, nbsp + } + } +} + +// Opposite alignment of blockquote +// +// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0. +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid $blockquote-border-color; + border-left: 0; + text-align: right; + + // Account for citation + footer, + small, + .small { + &:before { content: ''; } + &:after { + content: '\00A0 \2014'; // nbsp, em dash + } + } +} + +// Quotes +blockquote:before, +blockquote:after { + content: ""; +} + +// Addresses +address { + margin-bottom: $line-height-computed; + font-style: normal; + line-height: $line-height-base; +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_utilities.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_utilities.scss new file mode 100644 index 000000000..3ad5f2eed --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_utilities.scss @@ -0,0 +1,57 @@ +// +// Utility classes +// -------------------------------------------------- + + +// Floats +// ------------------------- + +.clearfix { + @include clearfix(); +} +.center-block { + @include center-block(); +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} + + +// Toggling content +// ------------------------- + +// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + @include text-hide(); +} + + +// Hide from screenreaders and browsers +// +// Credit: HTML5 Boilerplate + +.hidden { + display: none !important; + visibility: hidden !important; +} + + +// For Affix plugin +// ------------------------- + +.affix { + position: fixed; + @include translate3d(0, 0, 0); +} diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_variables.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_variables.scss new file mode 100644 index 000000000..93435f597 --- /dev/null +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_variables.scss @@ -0,0 +1,852 @@ +// When true, asset path helpers are used, otherwise the regular CSS `url()` is used. +// When there no function is defined, `fn('')` is parsed as string that equals the right hand side +// NB: in Sass 3.3 there is a native function: function-exists(twbs-font-path) +$bootstrap-sass-asset-helper: (twbs-font-path("") != unquote('twbs-font-path("")')) !default; + +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +$gray-darker: lighten(#000, 13.5%) !default; // #222 +$gray-dark: lighten(#000, 20%) !default; // #333 +$gray: lighten(#000, 33.5%) !default; // #555 +$gray-light: lighten(#000, 46.7%) !default; // #777 +$gray-lighter: lighten(#000, 93.5%) !default; // #eee + +$brand-primary: map-get($colors, orange) !default; +$brand-success: #9BD275 !default; +$brand-info: #B0CDDB !default; +$brand-warning: #f0ad4e !default; +$brand-danger: #F05050 !default; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for ``. +$body-bg: #fff !default; +//** Global text color on ``. +$text-color: map-get($colors, darkgrey) !default; + +//** Global textual link color. +$link-color: $brand-primary !default; +//** Link hover color set via `darken()` function. +$link-hover-color: darken($link-color, 15%) !default; + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +$font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif !default; +$font-family-serif: Georgia, "Times New Roman", Times, serif !default; +//** Default monospace fonts for ``, ``, and `
        `.
        +$font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace !default;
        +$font-family-base:        $font-family-sans-serif !default;
        +
        +$font-size-base:          14px !default;
        +$font-size-large:         ceil(($font-size-base * 1.25)) !default; // ~18px
        +$font-size-small:         ceil(($font-size-base * 0.85)) !default; // ~12px
        +
        +$font-size-h1:            floor(($font-size-base * 1.7)) !default; // ~36px
        +$font-size-h2:            floor(($font-size-base * 1.15)) !default; // ~30px
        +$font-size-h3:            ceil(($font-size-base * 1)) !default; // ~24px
        +$font-size-h4:            ceil(($font-size-base * 1.25)) !default; // ~18px
        +$font-size-h5:            $font-size-base !default;
        +$font-size-h6:            ceil(($font-size-base * 0.85)) !default; // ~12px
        +
        +//** Unit-less `line-height` for use in components like buttons.
        +$line-height-base:        1.428571429 !default; // 20/14
        +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
        +$line-height-computed:    floor(($font-size-base * $line-height-base)) !default; // ~20px
        +
        +//** By default, this inherits from the ``.
        +$headings-font-family:    "SourceSansProSemiBold" !default;
        +$headings-font-weight:    100 !default;
        +$headings-line-height:    1.4 !default;
        +$headings-color:          inherit !default;
        +
        +
        +//== Iconography
        +//
        +//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
        +
        +//** Load fonts from this directory.
        +
        +// [converter] Asset helpers such as Sprockets and Node.js Mincer do not resolve relative paths
        +$icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../fonts/bootstrap/") !default;
        +
        +//** File name for all font files.
        +$icon-font-name:          "glyphicons-halflings-regular" !default;
        +//** Element ID within SVG icon file.
        +$icon-font-svg-id:        "glyphicons_halflingsregular" !default;
        +
        +
        +//== Components
        +//
        +//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
        +
        +$padding-base-vertical:     6px !default;
        +$padding-base-horizontal:   12px !default;
        +
        +$padding-large-vertical:    10px !default;
        +$padding-large-horizontal:  16px !default;
        +
        +$padding-small-vertical:    5px !default;
        +$padding-small-horizontal:  10px !default;
        +
        +$padding-xs-vertical:       1px !default;
        +$padding-xs-horizontal:     5px !default;
        +
        +$line-height-large:         1.33 !default;
        +$line-height-small:         1.5 !default;
        +
        +$border-radius-base:        3px !default;
        +$border-radius-large:       6px !default;
        +$border-radius-small:       3px !default;
        +
        +//** Global color for active items (e.g., navs or dropdowns).
        +$component-active-color:    #fff !default;
        +//** Global background color for active items (e.g., navs or dropdowns).
        +$component-active-bg:       $brand-primary !default;
        +
        +//** Width of the `border` for generating carets that indicator dropdowns.
        +$caret-width-base:          4px !default;
        +//** Carets increase slightly in size for larger components.
        +$caret-width-large:         5px !default;
        +
        +
        +//== Tables
        +//
        +//## Customizes the `.table` component with basic values, each used across all table variations.
        +
        +//** Padding for ``s and ``s.
        +$table-cell-padding:            10px 0px 10px 20px !default;
        +//** Padding for cells in `.table-condensed`.
        +$table-condensed-cell-padding:  5px !default;
        +
        +//** Default background color used for all tables.
        +$table-bg:                      transparent !default;
        +//** Background color used for `.table-striped`.
        +$table-bg-accent:               map-get($colors, lightergrey) !default;
        +//** Background color used for `.table-hover`.
        +$table-bg-hover:                #f5f5f5 !default;
        +$table-bg-active:               $table-bg-hover !default;
        +
        +//** Border color for table and cell borders.
        +$table-border-color:            #eee !default;
        +
        +
        +//== Buttons
        +//
        +//## For each of Bootstrap's buttons, define text, background and border color.
        +
        +$btn-font-weight:                normal !default;
        +
        +$btn-default-color:              #444 !default;
        +$btn-default-bg:                 #FFFFFF !default;
        +$btn-default-border:             #e5e5e5 !default;
        +
        +$btn-primary-color:              #fff !default;
        +$btn-primary-bg:                 $brand-primary !default;
        +$btn-primary-border:             #D95100 !default;
        +
        +$btn-success-color:              #fff !default;
        +$btn-success-bg:                 $brand-success !default;
        +$btn-success-border:             darken($btn-success-bg, 5%) !default;
        +
        +$btn-info-color:                 #fff !default;
        +$btn-info-bg:                    $brand-info !default;
        +$btn-info-border:                darken($btn-info-bg, 5%) !default;
        +
        +$btn-warning-color:              #fff !default;
        +$btn-warning-bg:                 $brand-warning !default;
        +$btn-warning-border:             darken($btn-warning-bg, 5%) !default;
        +
        +$btn-danger-color:               #fff !default;
        +$btn-danger-bg:                  $brand-danger !default;
        +$btn-danger-border:              darken($btn-danger-bg, 5%) !default;
        +
        +$btn-link-disabled-color:        $gray-light !default;
        +
        +
        +//== Forms
        +//
        +//##
        +
        +//** `` background color
        +$input-bg:                       #fff !default;
        +//** `` background color
        +$input-bg-disabled:              $gray-lighter !default;
        +
        +//** Text color for ``s
        +$input-color:                    $gray !default;
        +//** `` border color
        +$input-border:                   #ccc !default;
        +//** `` border radius
        +$input-border-radius:            $border-radius-base !default;
        +//** Border color for inputs on focus
        +$input-border-focus:             #66afe9 !default;
        +
        +//** Placeholder text color
        +$input-color-placeholder:        $gray-light !default;
        +
        +//** Default `.form-control` height
        +$input-height-base:              ($line-height-computed + ($padding-base-vertical * 2) + 2) !default;
        +//** Large `.form-control` height
        +$input-height-large:             (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default;
        +//** Small `.form-control` height
        +$input-height-small:             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default;
        +
        +$legend-color:                   $gray-dark !default;
        +$legend-border-color:            #e5e5e5 !default;
        +
        +//** Background color for textual input addons
        +$input-group-addon-bg:           $gray-lighter !default;
        +//** Border color for textual input addons
        +$input-group-addon-border-color: $input-border !default;
        +
        +
        +//== Dropdowns
        +//
        +//## Dropdown menu container and contents.
        +
        +//** Background for the dropdown menu.
        +$dropdown-bg:                    #fff !default;
        +//** Dropdown menu `border-color`.
        +$dropdown-border:                rgba(0,0,0,.15) !default;
        +//** Dropdown menu `border-color` **for IE8**.
        +$dropdown-fallback-border:       #ccc !default;
        +//** Divider color for between dropdown items.
        +$dropdown-divider-bg:            #e5e5e5 !default;
        +
        +//** Dropdown link text color.
        +$dropdown-link-color:            $gray-dark !default;
        +//** Hover color for dropdown links.
        +$dropdown-link-hover-color:      darken($gray-dark, 5%) !default;
        +//** Hover background for dropdown links.
        +$dropdown-link-hover-bg:         #f5f5f5 !default;
        +
        +//** Active dropdown menu item text color.
        +$dropdown-link-active-color:     $component-active-color !default;
        +//** Active dropdown menu item background color.
        +$dropdown-link-active-bg:        $component-active-bg !default;
        +
        +//** Disabled dropdown menu item background color.
        +$dropdown-link-disabled-color:   $gray-light !default;
        +
        +//** Text color for headers within dropdown menus.
        +$dropdown-header-color:          $gray-light !default;
        +
        +//** Deprecated `$dropdown-caret-color` as of v3.1.0
        +$dropdown-caret-color:           #000 !default;
        +
        +
        +//-- Z-index master list
        +//
        +// Warning: Avoid customizing these values. They're used for a bird's eye view
        +// of components dependent on the z-axis and are designed to all work together.
        +//
        +// Note: These variables are not generated into the Customizer.
        +
        +$zindex-navbar:            1000 !default;
        +$zindex-dropdown:          1000 !default;
        +$zindex-popover:           1060 !default;
        +$zindex-tooltip:           1070 !default;
        +$zindex-navbar-fixed:      1030 !default;
        +$zindex-modal-background:  1040 !default;
        +$zindex-modal:             1050 !default;
        +
        +
        +//== Media queries breakpoints
        +//
        +//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
        +
        +// Extra small screen / phone
        +//** Deprecated `$screen-xs` as of v3.0.1
        +$screen-xs:                  480px !default;
        +//** Deprecated `$screen-xs-min` as of v3.2.0
        +$screen-xs-min:              $screen-xs !default;
        +//** Deprecated `$screen-phone` as of v3.0.1
        +$screen-phone:               $screen-xs-min !default;
        +
        +// Small screen / tablet
        +//** Deprecated `$screen-sm` as of v3.0.1
        +$screen-sm:                  768px !default;
        +$screen-sm-min:              $screen-sm !default;
        +//** Deprecated `$screen-tablet` as of v3.0.1
        +$screen-tablet:              $screen-sm-min !default;
        +
        +// Medium screen / desktop
        +//** Deprecated `$screen-md` as of v3.0.1
        +$screen-md:                  992px !default;
        +$screen-md-min:              $screen-md !default;
        +//** Deprecated `$screen-desktop` as of v3.0.1
        +$screen-desktop:             $screen-md-min !default;
        +
        +// Large screen / wide desktop
        +//** Deprecated `$screen-lg` as of v3.0.1
        +$screen-lg:                  1200px !default;
        +$screen-lg-min:              $screen-lg !default;
        +//** Deprecated `$screen-lg-desktop` as of v3.0.1
        +$screen-lg-desktop:          $screen-lg-min !default;
        +
        +// So media queries don't overlap when required, provide a maximum
        +$screen-xs-max:              ($screen-sm-min - 1) !default;
        +$screen-sm-max:              ($screen-md-min - 1) !default;
        +$screen-md-max:              ($screen-lg-min - 1) !default;
        +
        +
        +//== Grid system
        +//
        +//## Define your custom responsive grid.
        +
        +//** Number of columns in the grid.
        +$grid-columns:              12 !default;
        +//** Padding between columns. Gets divided in half for the left and right.
        +$grid-gutter-width:         40px !default;
        +// Navbar collapse
        +//** Point at which the navbar becomes uncollapsed.
        +$grid-float-breakpoint:     $screen-sm-min !default;
        +//** Point at which the navbar begins collapsing.
        +$grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default;
        +
        +
        +//== Container sizes
        +//
        +//## Define the maximum width of `.container` for different screen sizes.
        +
        +// Small screen / tablet
        +$container-tablet:             ((720px + $grid-gutter-width)) !default;
        +//** For `$screen-sm-min` and up.
        +$container-sm:                 $container-tablet !default;
        +
        +// Medium screen / desktop
        +$container-desktop:            ((940px + $grid-gutter-width)) !default;
        +//** For `$screen-md-min` and up.
        +$container-md:                 $container-desktop !default;
        +
        +// Large screen / wide desktop
        +$container-large-desktop:      ((1140px + $grid-gutter-width)) !default;
        +//** For `$screen-lg-min` and up.
        +$container-lg:                 $container-large-desktop !default;
        +
        +
        +//== Navbar
        +//
        +//##
        +
        +// Basics of a navbar
        +$navbar-height:                    50px !default;
        +$navbar-margin-bottom:             0;
        +$navbar-border-radius:             0;
        +$navbar-padding-horizontal:        floor((calc($grid-gutter-width / 2))) !default;
        +$navbar-padding-vertical:          15px;
        +$navbar-collapse-max-height:       340px !default;
        +
        +$navbar-default-color:             map-get($colors, lightgrey) !default;
        +$navbar-default-bg:                map-get($colors, darkgrey) !default;
        +$navbar-default-border:            darken($navbar-default-bg, 6.5%) !default;
        +
        +// Navbar links
        +$navbar-default-link-color:                map-get($colors, lightgrey) !default;
        +$navbar-default-link-hover-color:          map-get($colors, orange) !default;
        +$navbar-default-link-hover-bg:             transparent !default;
        +$navbar-default-link-active-color:         #555 !default;
        +$navbar-default-link-active-bg:            darken($navbar-default-bg, 6.5%) !default;
        +$navbar-default-link-disabled-color:       #ccc !default;
        +$navbar-default-link-disabled-bg:          transparent !default;
        +
        +// Navbar brand label
        +$navbar-default-brand-color:               $navbar-default-link-color !default;
        +$navbar-default-brand-hover-color:         darken($navbar-default-brand-color, 10%) !default;
        +$navbar-default-brand-hover-bg:            transparent !default;
        +
        +// Navbar toggle
        +$navbar-default-toggle-hover-bg:           #555 !default;
        +$navbar-default-toggle-icon-bar-bg:        #FFF !default;
        +$navbar-default-toggle-border-color:       #FFF !default;
        +
        +
        +// Inverted navbar
        +// Reset inverted navbar basics
        +$navbar-inverse-color:                      $gray-light !default;
        +$navbar-inverse-bg:                         #222 !default;
        +$navbar-inverse-border:                     darken($navbar-inverse-bg, 10%) !default;
        +
        +// Inverted navbar links
        +$navbar-inverse-link-color:                 $gray-light !default;
        +$navbar-inverse-link-hover-color:           #fff !default;
        +$navbar-inverse-link-hover-bg:              transparent !default;
        +$navbar-inverse-link-active-color:          $navbar-inverse-link-hover-color !default;
        +$navbar-inverse-link-active-bg:             darken($navbar-inverse-bg, 10%) !default;
        +$navbar-inverse-link-disabled-color:        #444 !default;
        +$navbar-inverse-link-disabled-bg:           transparent !default;
        +
        +// Inverted navbar brand label
        +$navbar-inverse-brand-color:                $navbar-inverse-link-color !default;
        +$navbar-inverse-brand-hover-color:          #fff !default;
        +$navbar-inverse-brand-hover-bg:             transparent !default;
        +
        +// Inverted navbar toggle
        +$navbar-inverse-toggle-hover-bg:            #333 !default;
        +$navbar-inverse-toggle-icon-bar-bg:         #fff !default;
        +$navbar-inverse-toggle-border-color:        #333 !default;
        +
        +
        +//== Navs
        +//
        +//##
        +
        +//=== Shared nav styles
        +$nav-link-padding:                          10px 15px !default;
        +$nav-link-hover-bg:                         $gray-lighter !default;
        +
        +$nav-disabled-link-color:                   $gray-light !default;
        +$nav-disabled-link-hover-color:             $gray-light !default;
        +
        +$nav-open-link-hover-color:                 #fff !default;
        +
        +//== Tabs
        +$nav-tabs-border-color:                     #E5E5E5 !default;
        +
        +$nav-tabs-link-hover-border-color:          $gray-lighter !default;
        +
        +$nav-tabs-active-link-hover-bg:             $body-bg !default;
        +$nav-tabs-active-link-hover-color:          $gray !default;
        +$nav-tabs-active-link-hover-border-color:   #ddd !default;
        +
        +$nav-tabs-justified-link-border-color:            #ddd !default;
        +$nav-tabs-justified-active-link-border-color:     $body-bg !default;
        +
        +//== Pills
        +$nav-pills-border-radius:                   0 !default;
        +$nav-pills-active-link-hover-bg:            $component-active-bg !default;
        +$nav-pills-active-link-hover-color:         $component-active-color !default;
        +
        +
        +//== Pagination
        +//
        +//##
        +
        +$pagination-color:                     $link-color !default;
        +$pagination-bg:                        #fff !default;
        +$pagination-border:                    #ddd !default;
        +
        +$pagination-hover-color:               $link-hover-color !default;
        +$pagination-hover-bg:                  $gray-lighter !default;
        +$pagination-hover-border:              #ddd !default;
        +
        +$pagination-active-color:              #fff !default;
        +$pagination-active-bg:                 $brand-primary !default;
        +$pagination-active-border:             $brand-primary !default;
        +
        +$pagination-disabled-color:            $gray-light !default;
        +$pagination-disabled-bg:               #fff !default;
        +$pagination-disabled-border:           #ddd !default;
        +
        +
        +//== Pager
        +//
        +//##
        +
        +$pager-bg:                             $pagination-bg !default;
        +$pager-border:                         $pagination-border !default;
        +$pager-border-radius:                  15px !default;
        +
        +$pager-hover-bg:                       $pagination-hover-bg !default;
        +
        +$pager-active-bg:                      $pagination-active-bg !default;
        +$pager-active-color:                   $pagination-active-color !default;
        +
        +$pager-disabled-color:                 $pagination-disabled-color !default;
        +
        +
        +//== Jumbotron
        +//
        +//##
        +
        +$jumbotron-padding:              30px !default;
        +$jumbotron-color:                inherit !default;
        +$jumbotron-bg:                   $gray-lighter !default;
        +$jumbotron-heading-color:        inherit !default;
        +$jumbotron-font-size:            ceil(($font-size-base * 1.5)) !default;
        +
        +
        +//== Form states and alerts
        +//
        +//## Define colors for form feedback states and, by default, alerts.
        +
        +$state-success-text:             $brand-success !default;
        +$state-success-bg:               $brand-success !default;
        +$state-success-border:           $brand-success !default;
        +
        +$state-info-text:                #31708f !default;
        +$state-info-bg:                  #d9edf7 !default;
        +$state-info-border:              darken(adjust-hue($state-info-bg, -10), 7%) !default;
        +
        +$state-warning-text:             $brand-warning !default;
        +$state-warning-bg:               #fcf8e3 !default;
        +$state-warning-border:           darken(adjust-hue($state-warning-bg, -10), 5%) !default;
        +
        +$state-danger-text:              $brand-danger !default;
        +$state-danger-bg:                $brand-danger !default;
        +$state-danger-border:            $brand-danger !default;
        +
        +
        +//== Tooltips
        +//
        +//##
        +
        +//** Tooltip max width
        +$tooltip-max-width:           200px !default;
        +//** Tooltip text color
        +$tooltip-color:               #fff !default;
        +//** Tooltip background color
        +$tooltip-bg:                  #000 !default;
        +$tooltip-opacity:             .9 !default;
        +
        +//** Tooltip arrow width
        +$tooltip-arrow-width:         5px !default;
        +//** Tooltip arrow color
        +$tooltip-arrow-color:         $tooltip-bg !default;
        +
        +
        +//== Popovers
        +//
        +//##
        +
        +//** Popover body background color
        +$popover-bg:                          #fff !default;
        +//** Popover maximum width
        +$popover-max-width:                   276px !default;
        +//** Popover border color
        +$popover-border-color:                rgba(0,0,0,.2) !default;
        +//** Popover fallback border color
        +$popover-fallback-border-color:       #ccc !default;
        +
        +//** Popover title background color
        +$popover-title-bg:                    darken($popover-bg, 3%) !default;
        +
        +//** Popover arrow width
        +$popover-arrow-width:                 10px !default;
        +//** Popover arrow color
        +$popover-arrow-color:                 #fff !default;
        +
        +//** Popover outer arrow width
        +$popover-arrow-outer-width:           ($popover-arrow-width + 1) !default;
        +//** Popover outer arrow color
        +$popover-arrow-outer-color:           fade_in($popover-border-color, 0.05) !default;
        +//** Popover outer arrow fallback color
        +$popover-arrow-outer-fallback-color:  darken($popover-fallback-border-color, 20%) !default;
        +
        +
        +//== Labels
        +//
        +//##
        +
        +//** Default label background color
        +$label-default-bg:            $gray-light !default;
        +//** Primary label background color
        +$label-primary-bg:            $brand-primary !default;
        +//** Success label background color
        +$label-success-bg:            $brand-success !default;
        +//** Info label background color
        +$label-info-bg:               $brand-info !default;
        +//** Warning label background color
        +$label-warning-bg:            $brand-warning !default;
        +//** Danger label background color
        +$label-danger-bg:             $brand-danger !default;
        +
        +//** Default label text color
        +$label-color:                 #fff !default;
        +//** Default text color of a linked label
        +$label-link-hover-color:      #fff !default;
        +
        +
        +//== Modals
        +//
        +//##
        +
        +//** Padding applied to the modal body
        +$modal-inner-padding:         15px !default;
        +
        +//** Padding applied to the modal title
        +$modal-title-padding:         15px !default;
        +//** Modal title line-height
        +$modal-title-line-height:     $line-height-base !default;
        +
        +//** Background color of modal content area
        +$modal-content-bg:                             #fff !default;
        +//** Modal content border color
        +$modal-content-border-color:                   rgba(0,0,0,.2) !default;
        +//** Modal content border color **for IE8**
        +$modal-content-fallback-border-color:          #999 !default;
        +
        +//** Modal backdrop background color
        +$modal-backdrop-bg:           #000 !default;
        +//** Modal backdrop opacity
        +$modal-backdrop-opacity:      .5 !default;
        +//** Modal header border color
        +$modal-header-border-color:   #e5e5e5 !default;
        +//** Modal footer border color
        +$modal-footer-border-color:   $modal-header-border-color !default;
        +
        +$modal-lg:                    900px !default;
        +$modal-md:                    600px !default;
        +$modal-sm:                    300px !default;
        +
        +
        +//== Alerts
        +//
        +//## Define alert colors, border radius, and padding.
        +
        +$alert-padding:               15px !default;
        +$alert-border-radius:         $border-radius-base !default;
        +$alert-link-font-weight:      bold !default;
        +
        +$alert-success-bg:            $state-success-bg !default;
        +$alert-success-text:          $state-success-text !default;
        +$alert-success-border:        $state-success-border !default;
        +
        +$alert-info-bg:               $state-info-bg !default;
        +$alert-info-text:             $state-info-text !default;
        +$alert-info-border:           $state-info-border !default;
        +
        +$alert-warning-bg:            $state-warning-bg !default;
        +$alert-warning-text:          $state-warning-text !default;
        +$alert-warning-border:        $state-warning-border !default;
        +
        +$alert-danger-bg:             $state-danger-bg !default;
        +$alert-danger-text:           $state-danger-text !default;
        +$alert-danger-border:         $state-danger-border !default;
        +
        +
        +//== Progress bars
        +//
        +//##
        +
        +//** Background color of the whole progress component
        +$progress-bg:                 #f5f5f5 !default;
        +//** Progress bar text color
        +$progress-bar-color:          #fff !default;
        +
        +//** Default progress bar color
        +$progress-bar-bg:             $brand-primary !default;
        +//** Success progress bar color
        +$progress-bar-success-bg:     $brand-success !default;
        +//** Warning progress bar color
        +$progress-bar-warning-bg:     $brand-warning !default;
        +//** Danger progress bar color
        +$progress-bar-danger-bg:      $brand-danger !default;
        +//** Info progress bar color
        +$progress-bar-info-bg:        $brand-info !default;
        +
        +
        +//== List group
        +//
        +//##
        +
        +//** Background color on `.list-group-item`
        +$list-group-bg:                 #fff !default;
        +//** `.list-group-item` border color
        +$list-group-border:             #ddd !default;
        +//** List group border radius
        +$list-group-border-radius:      $border-radius-base !default;
        +
        +//** Background color of single list items on hover
        +$list-group-hover-bg:           #f5f5f5 !default;
        +//** Text color of active list items
        +$list-group-active-color:       $component-active-color !default;
        +//** Background color of active list items
        +$list-group-active-bg:          $component-active-bg !default;
        +//** Border color of active list elements
        +$list-group-active-border:      $list-group-active-bg !default;
        +//** Text color for content within active list items
        +$list-group-active-text-color:  lighten($list-group-active-bg, 40%) !default;
        +
        +//** Text color of disabled list items
        +$list-group-disabled-color:      $gray-light !default;
        +//** Background color of disabled list items
        +$list-group-disabled-bg:         $gray-lighter !default;
        +//** Text color for content within disabled list items
        +$list-group-disabled-text-color: $list-group-disabled-color !default;
        +
        +$list-group-link-color:         map-get($colors, darkgrey) !default;
        +$list-group-link-hover-color:   map-get($colors, orange) !default;
        +$list-group-link-heading-color: #333 !default;
        +
        +
        +//== Panels
        +//
        +//##
        +
        +$panel-bg:                    #fff !default;
        +$panel-body-padding:          15px !default;
        +$panel-heading-padding:       10px 15px !default;
        +$panel-footer-padding:        $panel-heading-padding !default;
        +$panel-border-radius:         $border-radius-base !default;
        +
        +//** Border color for elements within panels
        +$panel-inner-border:          #ddd !default;
        +$panel-footer-bg:             #f5f5f5 !default;
        +
        +$panel-default-text:          $gray-dark !default;
        +$panel-default-border:        #ddd !default;
        +$panel-default-heading-bg:    #f5f5f5 !default;
        +
        +$panel-primary-text:          #fff !default;
        +$panel-primary-border:        $brand-primary !default;
        +$panel-primary-heading-bg:    $brand-primary !default;
        +
        +$panel-success-text:          $state-success-text !default;
        +$panel-success-border:        $state-success-border !default;
        +$panel-success-heading-bg:    $state-success-bg !default;
        +
        +$panel-info-text:             $state-info-text !default;
        +$panel-info-border:           $state-info-border !default;
        +$panel-info-heading-bg:       $state-info-bg !default;
        +
        +$panel-warning-text:          $state-warning-text !default;
        +$panel-warning-border:        $state-warning-border !default;
        +$panel-warning-heading-bg:    $state-warning-bg !default;
        +
        +$panel-danger-text:           $state-danger-text !default;
        +$panel-danger-border:         $state-danger-border !default;
        +$panel-danger-heading-bg:     $state-danger-bg !default;
        +
        +
        +//== Thumbnails
        +//
        +//##
        +
        +//** Padding around the thumbnail image
        +$thumbnail-padding:           4px !default;
        +//** Thumbnail background color
        +$thumbnail-bg:                $body-bg !default;
        +//** Thumbnail border color
        +$thumbnail-border:            #ddd !default;
        +//** Thumbnail border radius
        +$thumbnail-border-radius:     $border-radius-base !default;
        +
        +//** Custom text color for thumbnail captions
        +$thumbnail-caption-color:     $text-color !default;
        +//** Padding around the thumbnail caption
        +$thumbnail-caption-padding:   9px !default;
        +
        +
        +//== Wells
        +//
        +//##
        +
        +$well-bg:                     #f5f5f5 !default;
        +$well-border:                 darken($well-bg, 7%) !default;
        +
        +
        +//== Badges
        +//
        +//##
        +
        +$badge-color:                 #fff !default;
        +//** Linked badge text color on hover
        +$badge-link-hover-color:      #fff !default;
        +$badge-bg:                    $gray-light !default;
        +
        +//** Badge text color in active nav link
        +$badge-active-color:          $link-color !default;
        +//** Badge background color in active nav link
        +$badge-active-bg:             #fff !default;
        +
        +$badge-font-weight:           bold !default;
        +$badge-line-height:           1 !default;
        +$badge-border-radius:         10px !default;
        +
        +
        +//== Breadcrumbs
        +//
        +//##
        +
        +$breadcrumb-padding-vertical:   8px !default;
        +$breadcrumb-padding-horizontal: 15px !default;
        +//** Breadcrumb background color
        +$breadcrumb-bg:                 #f5f5f5 !default;
        +//** Breadcrumb text color
        +$breadcrumb-color:              #ccc !default;
        +//** Text color of current page in the breadcrumb
        +$breadcrumb-active-color:       $gray-light !default;
        +//** Textual separator for between breadcrumb elements
        +$breadcrumb-separator:          "/" !default;
        +
        +
        +//== Carousel
        +//
        +//##
        +
        +$carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6) !default;
        +
        +$carousel-control-color:                      #fff !default;
        +$carousel-control-width:                      15% !default;
        +$carousel-control-opacity:                    .5 !default;
        +$carousel-control-font-size:                  20px !default;
        +
        +$carousel-indicator-active-bg:                #fff !default;
        +$carousel-indicator-border-color:             #fff !default;
        +
        +$carousel-caption-color:                      #fff !default;
        +
        +
        +//== Close
        +//
        +//##
        +
        +$close-font-weight:           bold !default;
        +$close-color:                 #000 !default;
        +$close-text-shadow:           0 1px 0 #fff !default;
        +
        +
        +//== Code
        +//
        +//##
        +
        +$code-color:                  #c7254e !default;
        +$code-bg:                     #f9f2f4 !default;
        +
        +$kbd-color:                   #fff !default;
        +$kbd-bg:                      #333 !default;
        +
        +$pre-bg:                      #f5f5f5 !default;
        +$pre-color:                   $gray-dark !default;
        +$pre-border-color:            #ccc !default;
        +$pre-scrollable-max-height:   340px !default;
        +
        +
        +//== Type
        +//
        +//##
        +
        +//** Horizontal offset for forms and lists.
        +$component-offset-horizontal: 180px !default;
        +//** Text muted color
        +$text-muted:                  $gray-light !default;
        +//** Abbreviations and acronyms border color
        +$abbr-border-color:           $gray-light !default;
        +//** Headings small color
        +$headings-small-color:        $gray-light !default;
        +//** Blockquote small color
        +$blockquote-small-color:      $gray-light !default;
        +//** Blockquote font size
        +$blockquote-font-size:        ($font-size-base * 1.25) !default;
        +//** Blockquote border color
        +$blockquote-border-color:     $gray-lighter !default;
        +//** Page header border color
        +$page-header-border-color:    $gray-lighter !default;
        +//** Width of horizontal description list titles
        +$dl-horizontal-offset:        $component-offset-horizontal !default;
        +//** Horizontal line color.
        +$hr-border:                   $gray-lighter !default;
        diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_wells.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_wells.scss
        new file mode 100644
        index 000000000..b8657118a
        --- /dev/null
        +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/_wells.scss
        @@ -0,0 +1,29 @@
        +//
        +// Wells
        +// --------------------------------------------------
        +
        +
        +// Base class
        +.well {
        +  min-height: 20px;
        +  padding: 19px;
        +  margin-bottom: 20px;
        +  background-color: $well-bg;
        +  border: 1px solid $well-border;
        +  border-radius: $border-radius-base;
        +  @include box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
        +  blockquote {
        +    border-color: #ddd;
        +    border-color: rgba(0,0,0,.15);
        +  }
        +}
        +
        +// Sizes
        +.well-lg {
        +  padding: 24px;
        +  border-radius: $border-radius-large;
        +}
        +.well-sm {
        +  padding: 9px;
        +  border-radius: $border-radius-small;
        +}
        diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_alerts.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_alerts.scss
        new file mode 100644
        index 000000000..9a0b59007
        --- /dev/null
        +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_alerts.scss
        @@ -0,0 +1,14 @@
        +// Alerts
        +
        +@mixin alert-variant($background, $border, $text-color) {
        +  background-color: $background;
        +  border-color: $border;
        +  color: darken($text-color, 20%);
        +
        +  hr {
        +    border-top-color: darken($border, 5%);
        +  }
        +  .alert-link {
        +    color: darken($text-color, 15%);
        +  }
        +}
        diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_background-variant.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_background-variant.scss
        new file mode 100644
        index 000000000..4993bd2b8
        --- /dev/null
        +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_background-variant.scss
        @@ -0,0 +1,11 @@
        +// Contextual backgrounds
        +
        +// [converter] $parent hack
        +@mixin bg-variant($parent, $color) {
        +  #{$parent} {
        +    background-color: $color;
        +  }
        +  a#{$parent}:hover {
        +    background-color: darken($color, 10%);
        +  }
        +}
        diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_border-radius.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_border-radius.scss
        new file mode 100644
        index 000000000..ce1949987
        --- /dev/null
        +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_border-radius.scss
        @@ -0,0 +1,18 @@
        +// Single side border-radius
        +
        +@mixin border-top-radius($radius) {
        +  border-top-right-radius: $radius;
        +   border-top-left-radius: $radius;
        +}
        +@mixin border-right-radius($radius) {
        +  border-bottom-right-radius: $radius;
        +     border-top-right-radius: $radius;
        +}
        +@mixin border-bottom-radius($radius) {
        +  border-bottom-right-radius: $radius;
        +   border-bottom-left-radius: $radius;
        +}
        +@mixin border-left-radius($radius) {
        +  border-bottom-left-radius: $radius;
        +     border-top-left-radius: $radius;
        +}
        diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_buttons.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_buttons.scss
        new file mode 100644
        index 000000000..58ad13e50
        --- /dev/null
        +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_buttons.scss
        @@ -0,0 +1,50 @@
        +// Button variants
        +//
        +// Easily pump out default styles, as well as :hover, :focus, :active,
        +// and disabled options for all buttons
        +
        +@mixin button-variant($color, $background, $border) {
        +  color: $color;
        +  background-color: $background;
        +  border-color: $border;
        +
        +  &:hover,
        +  &:focus,
        +  &:active,
        +  &.active,
        +  .open > &.dropdown-toggle {
        +    color: $color;
        +    background-color: darken($background, 10%);
        +        border-color: darken($border, 12%);
        +  }
        +  &:active,
        +  &.active,
        +  .open > &.dropdown-toggle {
        +    background-image: none;
        +  }
        +  &.disabled,
        +  &[disabled],
        +  fieldset[disabled] & {
        +    &,
        +    &:hover,
        +    &:focus,
        +    &:active,
        +    &.active {
        +      background-color: $background;
        +          border-color: $border;
        +    }
        +  }
        +
        +  .badge {
        +    color: $background;
        +    background-color: $color;
        +  }
        +}
        +
        +// Button sizes
        +@mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) {
        +  padding: $padding-vertical $padding-horizontal;
        +  font-size: $font-size;
        +  line-height: $line-height;
        +  border-radius: $border-radius;
        +}
        diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_center-block.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_center-block.scss
        new file mode 100644
        index 000000000..e06fb5e27
        --- /dev/null
        +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_center-block.scss
        @@ -0,0 +1,7 @@
        +// Center-align a block level element
        +
        +@mixin center-block() {
        +  display: block;
        +  margin-left: auto;
        +  margin-right: auto;
        +}
        diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_clearfix.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_clearfix.scss
        new file mode 100644
        index 000000000..dc3e2ab42
        --- /dev/null
        +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_clearfix.scss
        @@ -0,0 +1,22 @@
        +// Clearfix
        +//
        +// For modern browsers
        +// 1. The space content is one way to avoid an Opera bug when the
        +//    contenteditable attribute is included anywhere else in the document.
        +//    Otherwise it causes space to appear at the top and bottom of elements
        +//    that are clearfixed.
        +// 2. The use of `table` rather than `block` is only necessary if using
        +//    `:before` to contain the top-margins of child elements.
        +//
        +// Source: http://nicolasgallagher.com/micro-clearfix-hack/
        +
        +@mixin clearfix() {
        +  &:before,
        +  &:after {
        +    content: " "; // 1
        +    display: table; // 2
        +  }
        +  &:after {
        +    clear: both;
        +  }
        +}
        diff --git a/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_forms.scss b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_forms.scss
        new file mode 100644
        index 000000000..ff72f0efc
        --- /dev/null
        +++ b/misc/theme-flexcolor/src/opnsense/www/themes/flexcolor/assets/stylesheets/bootstrap/mixins/_forms.scss
        @@ -0,0 +1,84 @@
        +// Form validation states
        +//
        +// Used in forms.less to generate the form validation CSS for warnings, errors,
        +// and successes.
        +
        +@mixin form-control-validation($text-color: #555, $border-color: #ccc, $background-color: #f5f5f5) {
        +  // Color the label and help text
        +  .help-block,
        +  .control-label,
        +  .radio,
        +  .checkbox,
        +  .radio-inline,
        +  .checkbox-inline  {
        +    color: $text-color;
        +  }
        +  // Set the border and box shadow on specific inputs to match
        +  .form-control {
        +    border-color: $border-color;
        +    @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work
        +    &:focus {
        +      border-color: darken($border-color, 10%);
        +      $shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten($border-color, 20%);
        +      @include box-shadow($shadow);
        +    }
        +  }
        +  // Set validation states also for addons
        +  .input-group-addon {
        +    color: $text-color;
        +    border-color: $border-color;
        +    background-color: $background-color;
        +  }
        +  // Optional feedback icon
        +  .form-control-feedback {
        +    color: $text-color;
        +  }
        +}
        +
        +
        +// Form control focus state
        +//
        +// Generate a customized focus state and for any input with the specified color,
        +// which defaults to the `$input-border-focus` variable.
        +//
        +// We highly encourage you to not customize the default value, but instead use
        +// this to tweak colors on an as-needed basis. This aesthetic change is based on
        +// WebKit's default styles, but applicable to a wider range of browsers. Its
        +// usability and accessibility should be taken into account with any change.
        +//
        +// Example usage: change the default blue border and shadow to white for better
        +// contrast against a dark gray background.
        +@mixin form-control-focus($color: $input-border-focus) {
        +  $color-rgba: rgba(red($color), green($color), blue($color), .6);
        +  &:focus {
        +    border-color: $color;
        +    outline: 0;
        +    @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px $color-rgba);
        +  }
        +}
        +
        +// Form control sizing
        +//
        +// Relative text size, padding, and border-radii changes for form controls. For
        +// horizontal sizing, wrap controls in the predefined grid classes. ` />
                                
                               
                             
        @@ -253,7 +252,7 @@ include("head.inc");
                               
                                 />
                                
                               
                             
        @@ -270,12 +269,12 @@ include("head.inc");
                                 endforeach;?>
                                
                                
                               
                             
                             
        -                       
        +                       
                               
                                
        -                       
        -                      
        -                    
        -                    
        -                       
        -                      
        -                        
        -                        
        -                      
        -                    
        -                    
        -                       
        -                      
        -                        
        -                        
        -                      
        -                    
        -                    
        -                       
        -                      
        -                        
        -                        
        -                      
        -                    
        -                    
        -                       
        -                      
        -                        
        -                        
        -                      
        -                    
        -                    
        -                       
        -                      
        -                        
        -                        
        -                      
        -                    
        -                    
        -                       
        -                      
        -                        
        -                      
        -                    
        -                    
        -                       
        -                      
        -                        />
        -                       
        -                      
        -                    
        -                    
        -                       
        -                      
        -                        />
        -                       
        -                      
        -                    
        -                    
        -                       
        -                      
        -                        />
        -                       
        +            
        + +
        +
        +
        + + + + + + + + + + + + + $permuser): ?> - + - + diff --git a/net/upnp/src/www/status_upnp.php b/net/upnp/src/www/status_upnp.php index 96ac60e80..d52bcd0b7 100644 --- a/net/upnp/src/www/status_upnp.php +++ b/net/upnp/src/www/status_upnp.php @@ -41,7 +41,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $rdr_entries = array(); -exec("/sbin/pfctl -aminiupnpd -sn", $rdr_entries, $pf_ret); +exec("/sbin/pfctl -a miniupnpd -s nat -P", $rdr_entries, $pf_ret); $service_hook = 'miniupnpd'; include("head.inc"); @@ -57,21 +57,21 @@ include("head.inc");
        -

        +

        -
        + /> + +
        +
        - - - - - - - + + + + + + + @@ -82,9 +82,9 @@ include("head.inc"); } ?> - + @@ -97,8 +97,10 @@ include("head.inc"); diff --git a/net/wol/Makefile b/net/wol/Makefile index b878db54f..19e6aa94d 100644 --- a/net/wol/Makefile +++ b/net/wol/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= wol PLUGIN_VERSION= 2.5 -PLUGIN_REVISION= 1 +PLUGIN_REVISION= 3 PLUGIN_DEPENDS= wol PLUGIN_COMMENT= Wake on LAN Service PLUGIN_MAINTAINER= franco@opnsense.org diff --git a/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/Wol.xml b/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/Wol.xml index d4fb19530..08c10cc1f 100644 --- a/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/Wol.xml +++ b/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/Wol.xml @@ -6,23 +6,23 @@ Y - N - + N + /^(?!0).*$/ Y - N - /^((?:[a-fA-F0-9]{2}:){5}(?:[a-fA-F0-9]{2}))$/ - 00:00:00:00:00:00 + N + /^((?:[a-fA-F0-9]{2}:){5}(?:[a-fA-F0-9]{2}))$/ + 00:00:00:00:00:00 Should be 6 groups of 2 hex characters (a-fA-F0-9) separated by ':' N - N - + N + diff --git a/net/wol/src/opnsense/mvc/app/views/OPNsense/Wol/index.volt b/net/wol/src/opnsense/mvc/app/views/OPNsense/Wol/index.volt index 0e4454452..61dd88fd0 100644 --- a/net/wol/src/opnsense/mvc/app/views/OPNsense/Wol/index.volt +++ b/net/wol/src/opnsense/mvc/app/views/OPNsense/Wol/index.volt @@ -41,16 +41,16 @@ $( document ).ready(function() { var grid = $("#grid-wol-settings").UIBootgrid( - { 'search':'/api/wol/wol/searchHost', - 'get':'/api/wol/wol/getHost/', - 'set':'/api/wol/wol/setHost/', - 'add':'/api/wol/wol/addHost/', - 'del':'/api/wol/wol/delHost/', + { 'search':'/api/wol/wol/search_host', + 'get':'/api/wol/wol/get_host/', + 'set':'/api/wol/wol/set_host/', + 'add':'/api/wol/wol/add_host/', + 'del':'/api/wol/wol/del_host/', 'options':{ selection:false, multiSelect:false, formatters: { - "commandswithwake": function (column, row) { + "commands": function (column, row) { return " " + " " + "" + @@ -111,7 +111,7 @@ $( document ).ready(function() { - + diff --git a/net/wol/src/opnsense/www/js/widgets/Metadata/WakeOnLan.xml b/net/wol/src/opnsense/www/js/widgets/Metadata/WakeOnLan.xml index 3ba307ae9..aa46ac5df 100644 --- a/net/wol/src/opnsense/www/js/widgets/Metadata/WakeOnLan.xml +++ b/net/wol/src/opnsense/www/js/widgets/Metadata/WakeOnLan.xml @@ -2,8 +2,8 @@ WakeOnLan.js - /api/diagnostics/interface/getArp* - /api/wol/wol/searchHost + /api/diagnostics/interface/get_arp* + /api/wol/wol/search_host /api/wol/wol/set diff --git a/net/wol/src/opnsense/www/js/widgets/WakeOnLan.js b/net/wol/src/opnsense/www/js/widgets/WakeOnLan.js index 988d60bd5..be5f12b57 100644 --- a/net/wol/src/opnsense/www/js/widgets/WakeOnLan.js +++ b/net/wol/src/opnsense/www/js/widgets/WakeOnLan.js @@ -47,7 +47,7 @@ export default class WakeOnLan extends BaseTableWidget { } async onWidgetTick() { - const data = await this.ajaxCall('/api/wol/wol/searchHost'); + const data = await this.ajaxCall('/api/wol/wol/search_host'); let rows = []; if (data.total == 0) { @@ -62,11 +62,11 @@ export default class WakeOnLan extends BaseTableWidget { //NOTE: this ARP list call is the most expensive one and can grow substantialy in big networks //With previous widget it had been done on the backend side with direct exec of 'arp -an | grep ...' - const arp = await this.ajaxCall(`/api/diagnostics/interface/getArp${''}`); + const arp = await this.ajaxCall(`/api/diagnostics/interface/get_arp${''}`); for(let it = 0; it < data.rows.length; it++){ const item = data.rows[it]; - let is_active = this.checkActive(arp, item.mac, item.interface); + let is_active = this.checkActive(arp, item.mac, item["%interface"]); let row = [ `${item.descr.length !== 0 ? item.descr + '
        ': ''} ${item.mac}`, `${item.interface}`, diff --git a/net/zerotier/Makefile b/net/zerotier/Makefile index aad751c0a..bd40a597b 100644 --- a/net/zerotier/Makefile +++ b/net/zerotier/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= zerotier PLUGIN_VERSION= 1.3.2 -PLUGIN_REVISION= 5 +PLUGIN_REVISION= 6 PLUGIN_COMMENT= Virtual Networks That Just Work PLUGIN_DEPENDS= zerotier PLUGIN_MAINTAINER= dharrigan@gmail.com diff --git a/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.xml b/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.xml index d081852fb..1a5b01014 100644 --- a/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.xml +++ b/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.xml @@ -6,11 +6,11 @@ 1.3.0 - 0 + 0 Y - + N @@ -19,15 +19,15 @@ - 0 + 0 Y - + Y - + N diff --git a/net/zerotier/src/opnsense/mvc/app/views/OPNsense/Zerotier/index.volt b/net/zerotier/src/opnsense/mvc/app/views/OPNsense/Zerotier/index.volt index 55eb6159e..f71aedc92 100644 --- a/net/zerotier/src/opnsense/mvc/app/views/OPNsense/Zerotier/index.volt +++ b/net/zerotier/src/opnsense/mvc/app/views/OPNsense/Zerotier/index.volt @@ -98,10 +98,10 @@ POSSIBILITY OF SUCH DAMAGE.
        - - . +
        {{ lang._('Interface') }} {{ lang._('MAC') }} {{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
        - - - - + + + + diff --git a/ruleset.xml b/ruleset.xml deleted file mode 100644 index ff88d20ce..000000000 --- a/ruleset.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - Slightly enhanced version of the PSR12 standard - - - - - *.css - *.js - diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile index f750997c0..711735a66 100644 --- a/security/acme-client/Makefile +++ b/security/acme-client/Makefile @@ -1,5 +1,6 @@ PLUGIN_NAME= acme-client -PLUGIN_VERSION= 4.8 +PLUGIN_VERSION= 4.10 +PLUGIN_REVISION= 1 PLUGIN_COMMENT= ACME Client PLUGIN_MAINTAINER= opnsense@moov.de PLUGIN_DEPENDS= acme.sh py${PLUGIN_PYTHON}-dns-lexicon diff --git a/security/acme-client/pkg-descr b/security/acme-client/pkg-descr index 5d07a9eae..f95410725 100644 --- a/security/acme-client/pkg-descr +++ b/security/acme-client/pkg-descr @@ -8,6 +8,37 @@ WWW: https://github.com/acmesh-official/acme.sh Plugin Changelog ================ +4.10 + +Added: +* new automation to reload www/caddy (#4692) +* new automation to upload to Proxmox Backup Server (#4785) +* add support for Websupport.sk DNS API (#4540) +* add SFTP option: Preserve Modification Time (#3862) + +Changed: +* automatically fix account config if CERT_HOME is set (#4622) +* automatically resolve cron job mismatch (#4627) +* change default SFTP options to NOT preserve modification time (#3862) +* migrate SFTP/SSH operations to AcmeClient logging +* add detailed SFTP/SSH logging when debug logging is enabled +* add new log messages and improve existing ones + +Fixed: +* deploy hooks may use the old CERT_HOME (#4622) +* acme.sh is always called with "--days 1" (#4711) +* avoid startup error: "rmdir... Not a directory" (#4743) +* fails to create/update cron job on UUID mismatch (#4627) +* accounts/certificates not visible on 25.7 alpha (#4790) + +Removed: +* remove stdout (CLI) logging from SFTP library + +4.9 + +Added: +* Add support for Scaleway DNS API (#4492) + 4.8 BREAKING CHANGE: Let's Encrypt ends support for the OCSP Must Staple diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php index 695239e5b..5251a32f7 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php @@ -1,7 +1,7 @@ getModel(); $backend = new Backend(); - // Setup cronjob if AcmeClient and AutoRenewal is enabled. + // Setup cron job if AcmeClient and AutoRenewal is enabled. if ( - (string)$mdlAcme->settings->UpdateCron == "" and (string)$mdlAcme->settings->autoRenewal == "1" and (string)$mdlAcme->settings->enabled == "1" ) { - $mdlCron = new Cron(); - // NOTE: Only configd actions are valid commands for cronjobs - // and they *must* provide a description that is not empty. - $cron_uuid = $mdlCron->newDailyJob( - "AcmeClient", - "acmeclient cron-auto-renew", - "AcmeClient Cronjob for Certificate AutoRenewal", - "*", - "1" - ); - $mdlAcme->settings->UpdateCron = $cron_uuid; + // Check if a cron job UUID is available. + $cron_uuid = (string)$mdlAcme->settings->UpdateCron; + $cron_found = 0; + if ($cron_uuid != "") { + // Try to get cron job data from system config. + $cron_job = (new Cron())->getNodeByReference('jobs.job.' . $cron_uuid); + if ($cron_job != null) { + // Cron job found, no changes required. + $cron_found = 1; + $this->getLogger()->notice("AcmeClient: successfully validated cron job"); + } else { + // Cron job NOT found. This should not happen, try to fix + // this automatically. + $this->getLogger()->error("AcmeClient: cron job with stored UUID not found in system config: ${cron_uuid}"); - // Save updated configuration. - if ($mdlCron->performValidation()->count() == 0) { - $mdlCron->serializeToConfig(); - // save data to config, do not validate because the current in memory model doesn't know about the - // cron item just created. - $mdlAcme->serializeToConfig($validateFullModel = false, $disable_validation = true); - Config::getInstance()->save(); - // Refresh the crontab - $backend->configdRun('template reload OPNsense/Cron'); - // (res)start daemon - $backend->configdRun("cron restart"); - $result['result'] = "new"; - $result['uuid'] = $cron_uuid; - } else { - $result['result'] = "unable to add cron"; + // Search for existing AcmeClient cron job. + foreach ((new Cron())->getNodeByReference('jobs.job')->iterateItems() as $cron) { + $_uuid = $cron->getAttributes()["uuid"]; + $_origin = (string)$cron->origin; + if ($_origin == 'AcmeClient') { + // Found a matching AcmeClient cron job. + $cron_found = 1; + $this->getLogger()->notice("AcmeClient: found existing AcmeClient cron job, fixing inconsistency in config (new UUID: ${_uuid})"); + // Update UUID in Acme Client config. + $mdlAcme->settings->UpdateCron = $_uuid; + // Save updated configuration. + // TODO: need to disable validation? + $mdlAcme->serializeToConfig(); + Config::getInstance()->save(); + $result['result'] = "new"; + $result['uuid'] = $_uuid; + break; + } + } + } + } + + // No matching cron job found. Create a new one. + if ($cron_found == 0) { + $this->getLogger()->notice("AcmeClient: no cron job for AutoRenewal found, creating a new one"); + $mdlCron = new Cron(); + // NOTE: Only configd actions are valid commands for cronjobs + // and they *must* provide a description that is not empty. + $cron_uuid = $mdlCron->newDailyJob( + "AcmeClient", + "acmeclient cron-auto-renew", + "AcmeClient Cronjob for Certificate AutoRenewal", + "*", + "1" + ); + $mdlAcme->settings->UpdateCron = $cron_uuid; + + // Save updated configuration. + if ($mdlCron->performValidation()->count() == 0) { + $mdlCron->serializeToConfig(); + // save data to config, do not validate because the current in memory model doesn't know about the + // cron item just created. + $mdlAcme->serializeToConfig($validateFullModel = false, $disable_validation = true); + Config::getInstance()->save(); + // Refresh the crontab + $backend->configdRun('template reload OPNsense/Cron'); + // (res)start daemon + $backend->configdRun("cron restart"); + $result['result'] = "new"; + $result['uuid'] = $cron_uuid; + } else { + $result['result'] = "unable to add cron"; + } } // Delete cronjob if AcmeClient or AutoRenewal is disabled. } elseif ( @@ -99,6 +139,7 @@ class SettingsController extends ApiMutableModelControllerBase ((string)$mdlAcme->settings->autoRenewal == "0" or (string)$mdlAcme->settings->enabled == "0") ) { + $this->getLogger()->notice("AcmeClient: plugin or AutoRenewal is disabled, removing existing cron job"); // Get UUID, clean existin entry $cron_uuid = (string)$mdlAcme->settings->UpdateCron; $mdlAcme->settings->UpdateCron = ""; diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml index a2fb434eb..93167a695 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml @@ -68,6 +68,12 @@ The path can be absolute or relative to home and must exist. Leave blank to not change path after login. + + action.sftp_modtime + + checkbox + Preserves modification times from the source file. Note that this is not supported by all SFTP servers and may cause the upload to fail, e.g. on VMware + action.sftp_chmod @@ -325,6 +331,47 @@ text The API token. Required. + + + header + + + + action.acme_proxmoxbs_user + + text + The user who owns the API key. Defaults to root. + + + action.acme_proxmoxbs_server + + text + The hostname of the proxmox BS node. + + + action.acme_proxmoxbs_port + + text + The port number the management interface is on. Defaults to 8007. + + + action.acme_proxmoxbs_realm + + text + The authentication realm the user authenticates with. Defaults to pam. + + + action.acme_proxmoxbs_tokenid + + text + The name of the API token created for the user account. Defaults to acme. + + + action.acme_proxmoxbs_tokenkey + + text + The API token. Required. + header diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml index 702aa67c9..d71044036 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml @@ -1779,6 +1779,21 @@ password + + + header + + + + validation.dns_websupport_api_key + + text + + + validation.dns_websupport_api_secret + + password + header @@ -1924,4 +1939,14 @@ password + + + header + + + + validation.dns_scaleway_token + + text + diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php index e643696f2..4eef9200a 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php @@ -1,7 +1,7 @@ config->key)) { - LeUtils::log_debug('creating account key for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('creating account key for ' . (string)$this->config->name); // Check if we have an account key in our configuration if (!empty((string)$this->config->key)) { - LeUtils::log_debug('exporting existing account key to filesystem for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('exporting existing account key to filesystem for ' . (string)$this->config->name); // Write key to disk file_put_contents($account_key_file, (string)base64_decode((string)$this->config->key)); chmod($account_key_file, 0600); return true; } else { - LeUtils::log_debug('generating a new account key for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('generating a new account key for ' . (string)$this->config->name); // Preparation to run acme client $proc_env = $this->acme_env; // add env variables @@ -116,7 +116,7 @@ class LeAccount extends LeCommon . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--accountkeylength %s', self::ACME_ACCOUNT_KEY_LENGTH) . ' ' . LeUtils::execSafe('--accountconf %s', $account_conf_file); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); // Run acme.sh command $result = LeUtils::run_shell_command($acmecmd, $proc_env); @@ -156,7 +156,7 @@ class LeAccount extends LeCommon LeUtils::log_error('failed to save account key for ' . (string)$this->config->name); return false; } - LeUtils::log_debug('successfully created account key for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('successfully created account key for ' . (string)$this->config->name); return true; } } @@ -194,11 +194,11 @@ class LeAccount extends LeCommon // Check if account is already registered if (!($this->isRegistered())) { - LeUtils::log_debug('starting account registration for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('starting account registration for ' . (string)$this->config->name); // Check if ACME External Account Binding (EAB) is enabled if (!empty((string)$this->config->eab_kid) && !empty((string)$this->config->eab_hmac)) { - LeUtils::log_debug('enabling ACME EAB for this account', $this->debug); + LeUtils::log_debug('enabling ACME EAB for this account'); $this->acme_args[] = LeUtils::execSafe('--eab-kid %s', $this->config->eab_kid); $this->acme_args[] = LeUtils::execSafe('--eab-hmac-key %s', $this->config->eab_hmac); } @@ -212,7 +212,7 @@ class LeAccount extends LeCommon . '--registeraccount ' . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--accountconf %s', $this->account_conf_file); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); // Run acme.sh command $result = LeUtils::run_shell_command($acmecmd, $proc_env); @@ -224,16 +224,16 @@ class LeAccount extends LeCommon return false; } - // Fix account config - $this->fixConfig(); - // Update account status. LeUtils::log('account registration successful for ' . $this->config->name); $this->setStatus(200); } else { - LeUtils::log_debug('account already registered: ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('account already registered: ' . (string)$this->config->name); } + // Always check (and fix) account config + $this->fixConfig(); + return true; } @@ -251,18 +251,21 @@ class LeAccount extends LeCommon // Parse config file and remove property $account_conf = parse_ini_file($account_conf_file); if (isset($account_conf['CERT_HOME'])) { + LeUtils::log('fixing invalid account config (CERT_HOME): ' . $this->config->name); unset($account_conf['CERT_HOME']); - } - // Convert array back to ini file format - $new_account_conf = array(); - foreach ($account_conf as $key => $value) { - $new_account_conf[] = "{$key}='{$value}'"; - } + // Convert array back to ini file format + $new_account_conf = array(); + foreach ($account_conf as $key => $value) { + $new_account_conf[] = "{$key}='{$value}'"; + } - // Write changes back to file - file_put_contents($account_conf_file, implode("\n", $new_account_conf) . "\n"); - chmod($account_conf_file, 0600); + // Write changes back to file + file_put_contents($account_conf_file, implode("\n", $new_account_conf) . "\n"); + chmod($account_conf_file, 0600); + } else { + LeUtils::log('account config is valid (CERT_HOME): ' . $this->config->name); + } } } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeProxmoxbs.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeProxmoxbs.php new file mode 100644 index 000000000..5a2c6b701 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeProxmoxbs.php @@ -0,0 +1,50 @@ +acme_env['DEPLOY_PROXMOXBS_USER'] = (string)$this->config->acme_proxmoxbs_user; + $this->acme_env['DEPLOY_PROXMOXBS_SERVER'] = (string)$this->config->acme_proxmoxbs_server; + $this->acme_env['DEPLOY_PROXMOXBS_SERVER_PORT'] = (string)$this->config->acme_proxmoxbs_port; + $this->acme_env['DEPLOY_PROXMOXBS_USER_REALM'] = (string)$this->config->acme_proxmoxbs_realm; + $this->acme_env['DEPLOY_PROXMOXBS_API_TOKEN_NAME'] = (string)$this->config->acme_proxmoxbs_tokenid; + $this->acme_env['DEPLOY_PROXMOXBS_API_TOKEN_KEY'] = (string)$this->config->acme_proxmoxbs_tokenkey; + $this->acme_args[] = '--deploy-hook proxmoxbs'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php index 138d64d81..0571a8e08 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php @@ -1,7 +1,7 @@ * All rights reserved. @@ -131,7 +131,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon . implode(' ', $this->acme_args); // Run acme.sh command - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); $result = LeUtils::run_shell_command($acmecmd, $proc_env); // acme.sh records the last used deploy hook and would automatically @@ -156,7 +156,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon if (!file_put_contents($filename, $contents)) { LeUtils::log_error('clearing recorded deploy hook from acme.sh failed (' . $filename . ')'); } else { - LeUtils::log_debug('cleared recorded deploy deploy hook from acme.sh (' . $filename . ')', $this->debug); + LeUtils::log_debug('cleared recorded deploy deploy hook from acme.sh (' . $filename . ')'); } } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdReloadCaddy.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdReloadCaddy.php new file mode 100644 index 000000000..3acd8c008 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdReloadCaddy.php @@ -0,0 +1,45 @@ +command = 'caddy reload'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCertificate.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCertificate.php index 518a2c0f7..e45f92ded 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCertificate.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCertificate.php @@ -358,7 +358,7 @@ class LeCertificate extends LeCommon $configdir = (string)sprintf(self::ACME_CONFIG_DIR, (string)$this->config->id); foreach (array($certdir, $keydir, $configdir) as $dir) { if (!is_dir($dir)) { - LeUtils::log_debug("creating directory: {$dir}", $this->debug); + LeUtils::log_debug("creating directory: {$dir}"); mkdir($dir, 0700, true); } } @@ -467,7 +467,7 @@ class LeCertificate extends LeCommon . '--remove ' . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--domain %s', (string)$this->config->name); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); // Run acme.sh command $result = LeUtils::run_shell_command($acmecmd, $proc_env); @@ -542,7 +542,7 @@ class LeCertificate extends LeCommon . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--domain %s', (string)$this->config->name) . ' ' . LeUtils::execSafe('--accountconf %s', $account_conf_file); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); // Run acme.sh command $result = LeUtils::run_shell_command($acmecmd, $proc_env); @@ -618,6 +618,8 @@ class LeCertificate extends LeCommon $this->loadConfig(self::CONFIG_PATH, $this->uuid); } LeUtils::log('account is registered: ' . (string)$account->config->name); + // Always check (and fix) account config + $account->fixConfig(); return true; } @@ -642,7 +644,8 @@ class LeCertificate extends LeCommon // Configure validation object $val->setNames($this->config->name, $this->config->altNames, $this->config->aliasmode, $this->config->domainalias, $this->config->challengealias); - $val->setRenewal((int)$this->config->renewInterval); + $renewInterval = (string)$this->config->renewInterval; + $val->setRenewal((int)$renewInterval); $val->setForce($this->force); $val->setOcsp((string)$this->config->ocsp == 1 ? true : false); // strip prefix from key value diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php index 6dfd33eff..50ca996d8 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php @@ -32,6 +32,7 @@ namespace OPNsense\AcmeClient; use OPNsense\Core\Config; +use OPNsense\AcmeClient\AcmeClient; /** * Helper functions for LeAcme @@ -81,8 +82,13 @@ class LeUtils /** * log additional debug output */ - public static function log_debug($msg, bool $debug = false) + public static function log_debug($msg) { + $log_config = (new AcmeClient())->getNodeByReference('settings.logLevel'); + if (strpos($log_config, "debug") !== false) { + $debug = true; + } + if ($debug) { syslog(LOG_NOTICE, "AcmeClient: {$msg}"); } @@ -91,9 +97,14 @@ class LeUtils /** * log error messages */ - public static function log_error($msg) + public static function log_error($msg, $error = null) { - syslog(LOG_ERR, "AcmeClient: {$msg}"); + syslog( + LOG_ERR, + $error + ? ("AcmeClient: $msg; Trace: " . json_encode($error, JSON_UNESCAPED_SLASHES)) + : "AcmeClient: $msg" + ); } /** diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php index b28effbb6..6163abadd 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php @@ -1,7 +1,7 @@ * All rights reserved. @@ -167,7 +167,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon . "--{$acme_action} " . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--accountconf %s', $account_conf_file); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); // Run acme.sh command $result = LeUtils::run_shell_command($acmecmd, $proc_env); @@ -286,6 +286,6 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon */ public function setRenewal(int $interval = 60) { - $this->acme_args[] = LeUtils::execSafe('--days %s', (string)$interval); + $this->acme_args[] = LeUtils::execSafe('--days %s', $interval); } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsScaleway.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsScaleway.php new file mode 100644 index 000000000..22f237ac9 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsScaleway.php @@ -0,0 +1,44 @@ +acme_env['SCALEWAY_API_TOKEN'] = (string)$this->config->dns_scaleway_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsWebsupport.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsWebsupport.php new file mode 100644 index 000000000..83393e3eb --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsWebsupport.php @@ -0,0 +1,44 @@ +acme_env['WS_ApiKey'] = (string)$this->config->dns_websupport_api_key; + $this->acme_env['WS_ApiSecret'] = (string)$this->config->dns_websupport_api_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php index 12ab4ba56..8670eb96c 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php @@ -126,18 +126,17 @@ class HttpOpnsense extends Base implements LeValidationInterface // Create temporary port forward to allow acme challenges to get through $anchor_setup = "rdr-anchor \"acme-client\"\n"; - file_put_contents("{$configdir}/acme_anchor_setup", $anchor_setup); - chmod("{$configdir}/acme_anchor_setup", 0600); - mwexec("/sbin/pfctl -f {$configdir}/acme_anchor_setup"); - file_put_contents("{$configdir}/acme_anchor_rules", $anchor_rules); - chmod("{$configdir}/acme_anchor_rules", 0600); - mwexec("/sbin/pfctl -a acme-client -f {$configdir}/acme_anchor_rules"); + // XXX Should not be using util.inc from here + file_safe("{$configdir}/acme_anchor_setup", $anchor_setup, 0600); + mwexecf('/sbin/pfctl -f %s', ["{$configdir}/acme_anchor_setup"]); + file_safe("{$configdir}/acme_anchor_rules", $anchor_rules, 0600); + mwexecf('/sbin/pfctl -a %s -f %s', ['acme-client', "{$configdir}/acme_anchor_rules"]); } public function cleanup() { // Flush OPNsense port forward rules. - mwexec('/sbin/pfctl -a acme-client -F all'); + mwexecf('/sbin/pfctl -a %s -F %s', ['acme-client', 'all']); // Workaround to solve disconnection issues reported by some users. $backend = new \OPNsense\Core\Backend(); diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php index 03a4f9d76..068adc823 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php @@ -127,18 +127,17 @@ class TlsalpnAcme extends Base implements LeValidationInterface // Create temporary port forward to allow acme challenges to get through $anchor_setup = "rdr-anchor \"acme-client\"\n"; - file_put_contents("{$configdir}/acme_anchor_setup", $anchor_setup); - chmod("{$configdir}/acme_anchor_setup", 0600); - mwexec("/sbin/pfctl -f {$configdir}/acme_anchor_setup"); - file_put_contents("{$configdir}/acme_anchor_rules", $anchor_rules); - chmod("{$configdir}/acme_anchor_rules", 0600); - mwexec("/sbin/pfctl -a acme-client -f {$configdir}/acme_anchor_rules"); + // XXX Should not be using util.inc from here + file_safe("{$configdir}/acme_anchor_setup", $anchor_setup, 0600); + mwexecf('/sbin/pfctl -f %s', ["{$configdir}/acme_anchor_setup"]); + file_safe("{$configdir}/acme_anchor_rules", $anchor_rules, 0600); + mwexecf("/sbin/pfctl -a %s -f %s", ['acme-client', "{$configdir}/acme_anchor_rules"]); } public function cleanup() { // Flush OPNsense port forward rules. - mwexec('/sbin/pfctl -a acme-client -F all'); + mwexecf('/sbin/pfctl -a %s -F %s', ['acme-client', 'all']); // Workaround to solve disconnection issues reported by some users. $backend = new \OPNsense\Core\Backend(); diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Process.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Process.php index 59ed4808f..0a0b150fe 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Process.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Process.php @@ -1,6 +1,7 @@ error("Terminating process: " . json_encode(proc_get_status($handle))); + LeUtils::log_error("Terminating process: " . json_encode(proc_get_status($handle))); @proc_terminate($handle); } } @@ -102,7 +105,7 @@ class Process self::manageOpenedProcess($this->handle); } else { - Utils::log()->error("Failed opening '$cmd' in '$cwd'"); + LeUtils::log_error("Failed opening '$cmd' in '$cwd'"); } } @@ -181,7 +184,7 @@ class Process { // Read up-to 10k remaining lines from STDOUT/ERR to release locks before closing. for ($i = 0; ($line = $this->get(0)) && $i < 10000; $i++) { - Utils::log()->error("WARN: process: $line"); + LeUtils::log_error("WARN: process: $line"); } if ($this->isRunning()) { diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SSHKeys.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SSHKeys.php index 3d91cef69..ac70602d2 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SSHKeys.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SSHKeys.php @@ -1,6 +1,7 @@ info("No host key specified, using existing known_hosts entry for '$host'"); + LeUtils::log_debug("No host key specified, using existing known_hosts entry for '$host'"); $host_key = $known_by_host["key_info"]; } else { - Utils::log()->info("No host key specified and existing entry for '$host' cannot be used as isn't matching port $port."); + LeUtils::log_debug("No host key specified and existing entry for '$host' cannot be used as isn't matching port $port."); } } @@ -165,7 +168,7 @@ class SSHKeys $is_key_known = true; } elseif ($known_by_key) { if (strcasecmp(trim($host), trim($known_by_key["host"])) != 0) { - Utils::log()->info("Host key is in known_hosts but hostname differs. Changing '$host' to '{$known_by_key["host"]}'."); + LeUtils::log_debug("Host key is in known_hosts but hostname differs. Changing '$host' to '{$known_by_key["host"]}'."); $host = $known_by_key["host"]; } $is_key_known = true; @@ -196,15 +199,15 @@ class SSHKeys if (!empty($matching_remote_host_keys)) { if ($known_by_host && $known_by_host_matches_port) { - Utils::log()->info("Removing known_hosts entry with differing key for '{$known_by_host["host_query"]}' as it is in the way."); + LeUtils::log_debug("Removing known_hosts entry with differing key for '{$known_by_host["host_query"]}' as it is in the way."); $this->removeKnownHost($known_by_host["host_query"]); } foreach ($matching_remote_host_keys as $key) { - Utils::log()->info("Adding known_hosts entry: " . json_encode($key["key_info"], JSON_UNESCAPED_SLASHES)); + LeUtils::log_debug("Adding known_hosts entry: " . json_encode($key["key_info"], JSON_UNESCAPED_SLASHES)); $ok = file_put_contents($this->knownHostsFile(), $key["host_key"] . PHP_EOL, FILE_APPEND); if (!$ok) { - Utils::log()->error("Failed adding known_hosts entry {$key["host_key"]}"); + LeUtils::log_error("Failed adding SSH known_hosts entry {$key["host_key"]}"); } } @@ -331,12 +334,12 @@ class SSHKeys ? "" : PHP_EOL . "ssh-keyscan: " . join(PHP_EOL . "ssh-keyscan: ", $lines); - Utils::log()->error("Failed querying host keys ($key_type) for [$names] port $port. Exit code: {$p->exitCode} (error-marker: $marker) $output"); + LeUtils::log_error("Failed querying SSH host keys ($key_type) for [$names] port $port. Exit code: {$p->exitCode} (error-marker: $marker) $output"); } } if (empty($keys)) { - Utils::log()->info("Couldn't fetch public host key ($key_type) from {$host}:{$port}"); + LeUtils::log_debug("Couldn't fetch public host key ($key_type) from {$host}:{$port}"); if (!is_array($error) || empty($error)) { $error = ["connection_refused" => true]; @@ -384,13 +387,13 @@ class SSHKeys ? "" : PHP_EOL . join(PHP_EOL, $lines); - Utils::log()->error("Failed querying known hosts for $name_or_ip ($host). Exit code: {$p->exitCode} $output"); + LeUtils::log_error("Failed querying SSH known hosts for $name_or_ip ($host). Exit code: {$p->exitCode} $output"); } } } if (empty($keys)) { - Utils::log()->info("Didn't find $host in known_hosts"); + LeUtils::log_debug("Could not find $host in known_hosts"); } return $keys; @@ -408,7 +411,7 @@ class SSHKeys if ($p = Process::open(["ssh-keygen", "-R", $host, "-f", $this->knownHostsFile()])) { $ok = $p->close() === 0; if (!$ok) { - Utils::log()->error("Failed removing known hosts for $host. Return code was: {$p->exitCode}"); + LeUtils::log_error("Failed removing SSH known hosts for $host. Return code was: {$p->exitCode}"); } } @@ -433,11 +436,11 @@ class SSHKeys "key_length" => $matches[1] ]; } else { - Utils::log()->error("Unsupported hash type: $hash"); + LeUtils::log_error("Unsupported hash type: $hash"); } } - Utils::log()->error("Failed getting hash for host_key"); + LeUtils::log_error("Failed getting hash for host_key"); return false; } @@ -472,7 +475,7 @@ class SSHKeys if ($p = Process::open($generate_key)) { while (($line = $p->get(10)) !== false) { - Utils::log()->info("SSH keygen: $line"); + LeUtils::log_debug("SSH keygen: $line"); } Utils::requireThat( diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpClient.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpClient.php index 1804596af..ff3982dc6 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpClient.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpClient.php @@ -1,6 +1,7 @@ failed_status = ["invalid_parameters" => true]; - Utils::log()->error("Failed connecting to '$host'. Hostname or username is missing."); + LeUtils::log_error("Failed connecting to '$host'. Hostname or username is missing."); return false; } $trust = $this->ssh_keys->trustHost($host, $host_key, $port); if ($trust["ok"] !== true) { - Utils::log()->error("Failed establishing trust in '$host'; Cause: {$trust["error"]}"); + LeUtils::log_error("Failed establishing trust in '$host'; Cause: {$trust["error"]}"); unset($trust["ok"]); $this->failed_status = array_merge($trust, ["host_not_trusted" => true]); return false; @@ -103,7 +106,7 @@ class SftpClient "-oPreferredAuthentications=publickey" ); } else { - Utils::log()->error("Failed adding client identity ($identity). Connect will likely fail."); + LeUtils::log_error("Failed adding client identity ($identity). Connect will likely fail."); } // Adding the host @@ -113,7 +116,7 @@ class SftpClient if ($this->process = Process::open($cmd)) { $this->processAvailableInput(self::CONNECT_REPLY_TIMEOUT, 1, null, 0.75); if (($error = $this->lastError()) || !$this->process->isRunning()) { - Utils::log()->error("Failed connecting to '$host' (user: '$username')", $error); + LeUtils::log_error("Failed connecting to '$host' (user: '$username')", $error); return false; } $this->connection_info = ["host" => $host, "port" => $port, "user" => $username]; @@ -151,7 +154,7 @@ class SftpClient $consumed = ($lines_consumer && $lines_consumer($line) === true); if (!$consumed) { - Utils::log()->info("SFTP: " . rtrim($line)); + LeUtils::log_debug("SFTP: " . rtrim($line)); } if (!$lines_consumer || $consumed) { @@ -286,7 +289,7 @@ class SftpClient . (empty($remote_file) ? "" : " " . escapeshellarg($remote_file))); $this->processAvailableInput(self::COMMAND_REPLY_TIMEOUT, 2); } else { - Utils::log()->info("put: File $local_file doesn't exist."); + LeUtils::log_debug("put: File $local_file doesn't exist."); $this->failed_status = ["file_not_found" => true, "error" => $local_file]; } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpUploader.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpUploader.php index 353495880..bfd4b1885 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpUploader.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpUploader.php @@ -1,6 +1,7 @@ $local_file, "target" => $remote_file, "mode" => $chmod, - "group" => $chgrp + "group" => $chgrp, + "modtime" => $modtime ]; return $local_file; @@ -94,9 +99,10 @@ class SftpUploader * @param int $content_last_modified the unix timestamp when the content was last modified (is preserved when chmod is also specified). * @param bool $chmod the 4 digit unix permission to apply or false to leave it unchanged. * @param bool $chgrp the numeric group on the remote server to apply or false to leave it unchanged. + * @param bool $modtime a boolean to indicate that the modification times should be preserved. * @return string the name of the remote file. */ - public function addContent(string $content, string $remote_file = "", $content_last_modified = 0, $chmod = false, $chgrp = false): string + public function addContent(string $content, string $remote_file = "", $content_last_modified = 0, $chmod = false, $chgrp = false, $modtime = false): string { $local_file = $this->temporaryFile(); Utils::requireThat($local_file, "Failed creating temporary file for '$remote_file'"); @@ -113,7 +119,7 @@ class SftpUploader $remote_file = basename($local_file); } - $local_file = $this->addFile($local_file, $remote_file, $chmod, $chgrp); + $local_file = $this->addFile($local_file, $remote_file, $chmod, $chgrp, $modtime); $this->pending_files[$local_file]["delete_source"] = true; return $remote_file; @@ -153,7 +159,7 @@ class SftpUploader ->lastError(); if ($error) { - Utils::log()->error("Cannot continue since changing to initial remote path '{$this->pending_base_path}' failed", $error); + LeUtils::log_error("Cannot continue with SFTP upload since changing to initial remote path '{$this->pending_base_path}' failed", $error); return self::UPLOAD_ERROR; } } @@ -183,7 +189,7 @@ class SftpUploader try { $connection = $this->sftp->connected(); if (!$connection) { - Utils::log()->error("The sftp client is not connected, upload stopped."); + LeUtils::log_error("The SFTP client is not connected, upload stopped."); return self::UPLOAD_ERROR; } @@ -205,7 +211,7 @@ class SftpUploader foreach ($dir_names as $dir) { if ($error = $this->sftp->cd($dir)->lastError()) { if ($error["file_not_found"]) { - Utils::log()->info("Creating remote directory: $dir"); + LeUtils::log_debug("SFTP creating remote directory: $dir"); $this->sftp->clearError() ->mkdir($dir) ->cd($dir); @@ -216,7 +222,7 @@ class SftpUploader } if ($error = $this->sftp->lastError()) { - Utils::log()->error("Failed to cd into '$target_dir'.", $error); + LeUtils::log_error("SFTP failed to cd into '$target_dir'", $error); return self::UPLOAD_ERROR; } @@ -228,7 +234,7 @@ class SftpUploader if (empty($remote_files)) { $remote_files = $this->sftp->clearError()->ls(); if ($error = $this->sftp->lastError()) { - Utils::log()->error("Failed listing remote files.", $error); + LeUtils::log_error("SFTP failed listing remote files", $error); return self::UPLOAD_ERROR; } } @@ -239,10 +245,11 @@ class SftpUploader $remote_file = $remote_files[$remote_filename] ?? ["type" => "-", "owner" => $username]; $remote_is_file = $remote_file["type"] === "-"; $remote_is_readonly = preg_match('/^[^wW]+$/', $remote_file["permissions"] ?? ""); + LeUtils::log_debug("SFTP current remote file permissions: '{$remote_file["permissions"]}'"); // Check if a folder/socket/symlink, etc is in the way if (!$remote_is_file) { - Utils::log()->error("Failed uploading file '{$local_file}' as there is a non-file in the way at '{$file["target"]}'"); + LeUtils::log_error("SFTP failed uploading file '{$local_file}' as there is a non-file in the way at '{$file["target"]}'"); return self::UPLOAD_ERROR_NO_OVERWRITE; } @@ -252,16 +259,25 @@ class SftpUploader $chmod = $file["mode"] ?? ""; $chmod = preg_match('/^0\d{3}$/', $chmod) ? (string)$chmod : false; + // Preserving the modification time is not supported by all SFTP servers. + $preserve = $file["modtime"]; + if ($preserve !== false) { + LeUtils::log("SFTP upload will try to preserve file modification time for '{$file["target"]}'"); + } else { + LeUtils::log("SFTP upload will not preserve file modification time for '{$file["target"]}'"); + } // Initial upload when permissions are properly set. $should_upload_with_permission_change = $chmod !== false && isset($remote_files[$remote_filename]); + // Upload file. if (!$remote_is_readonly) { + LeUtils::log("Uploading file '{$local_file}' to '{$file["target"]}'"); $preserve_times_and_mod = $chmod !== false; - if ($error = $this->sftp->put($local_file, $remote_filename, $preserve_times_and_mod)->lastError()) { + if ($error = $this->sftp->put($local_file, $remote_filename, $preserve)->lastError()) { if ($error["permission_denied"] !== true) { $should_upload_with_permission_change = false; } @@ -269,7 +285,7 @@ class SftpUploader if ($should_upload_with_permission_change) { $this->sftp->clearError(); } else { - Utils::log()->error("Failed uploading file '{$local_file}' to '{$file["target"]}'", $error); + LeUtils::log_error("SFTP failed uploading file '{$local_file}' to '{$file["target"]}'", $error); return self::UPLOAD_ERROR_NO_PERMISSION; } } else { @@ -279,19 +295,21 @@ class SftpUploader // Second attempt when initial failed or was skipped due to write protection (only possible if we have chmod defined to reset permissions later) if ($should_upload_with_permission_change && $this->isFileOwnedByConnection($remote_file, $connection)) { - Utils::log()->info("Trying to upload file '{$local_file}' to '{$file["target"]}' with adjusted permissions"); + LeUtils::log("Trying to upload file '{$local_file}' to '{$file["target"]}' with adjusted permissions"); + // Change file permission to make it writable. if ($error = $this->sftp->chmod($remote_filename, '0600')->lastError()) { - Utils::log()->error("Failed changing permission to '0600' for '{$file["target"]}'. ", $error); + LeUtils::log_error("SFTP failed changing permission to '0600' for '{$file["target"]}'", $error); $this->sftp->clearError(); } - if ($error = $this->sftp->put($local_file, $remote_filename)->lastError()) { - Utils::log()->error("Failed uploading file (with adjusted permissions) '{$local_file}' to '{$file["target"]}'", $error); + // Try again to upload file. + if ($error = $this->sftp->put($local_file, $remote_filename, $preserve)->lastError()) { + LeUtils::log_error("SFTP failed uploading file (with adjusted permissions) '{$local_file}' to '{$file["target"]}'", $error); return self::UPLOAD_ERROR_NO_PERMISSION; } } elseif ($remote_is_readonly) { - Utils::log()->error("Failed uploading file '{$local_file}' to '{$file["target"]}'. Existing file is write protected."); + LeUtils::log_error("SFTP failed uploading file '{$local_file}' to '{$file["target"]}'. Existing file is write protected."); return self::UPLOAD_ERROR_NO_PERMISSION; } @@ -299,14 +317,14 @@ class SftpUploader // Applying chmod / chgrp if requested. if ($chmod) { if ($error = $this->sftp->chmod($remote_filename, $chmod)->lastError()) { - Utils::log()->error("Failed chmod ($chmod) for '{$file["target"]}'", $error); + LeUtils::log_error("SFTP failed chmod ($chmod) for '{$file["target"]}'", $error); return self::UPLOAD_ERROR_CHMOD_FAILED; } } if ($chgrp) { if ($error = $this->sftp->chgrp($remote_filename, $chgrp)->lastError()) { - Utils::log()->error("Failed chgrp ($chgrp) for '{$file["target"]}'", $error); + LeUtils::log_error("SFTP failed chgrp ($chgrp) for '{$file["target"]}'", $error); return self::UPLOAD_ERROR_CHGRP_FAILED; } } @@ -343,8 +361,9 @@ class SftpUploader $this->sftp->clearError(); - if ($error = $this->sftp->put($local_test_file, $remote_test_file)->lastError()) { - Utils::log()->error("Failed uploading test file to detect ownership. Next uploads may fail as well.", $error); + // Perform test upload without preserving file modification time (extra safekeeping). + if ($error = $this->sftp->put($local_test_file, $remote_test_file, false)->lastError()) { + LeUtils::log_error("Failed uploading SFTP test file to detect ownership. Next uploads may fail as well", $error); } else { // Get owner of the test file $file_info = $this->sftp->ls()[$remote_test_file] ?? ["owner" => -1]; @@ -400,7 +419,7 @@ class SftpUploader } if ($count > 0) { - Utils::log()->info("Removed $count files in shutdown hook instead of object destruction."); + LeUtils::log_debug("Removed $count files in shutdown hook instead of object destruction."); } $shared_temporary_files = []; diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Utils.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Utils.php index b5842d58f..a06e7b806 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Utils.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Utils.php @@ -1,6 +1,7 @@ error("FATAL: $message"); + LeUtils::log_error("FATAL: $message"); throw new \AssertionError($message); } return $expression; @@ -193,7 +133,6 @@ class Utils { static $options = [ "-h, --help Print commandline help", - "--log Enable log to stdout (instead of syslog)", "--automation-id Read options from the action specified by id or uuid", "--no-error Always exit with 0 (original exit codes are still logged)", ]; @@ -241,7 +180,7 @@ class Utils { global $argv; $command = self::getSelectedCLICommand($commands); - $options = ["help", "log", "no-error"]; + $options = ["help", "no-error"]; $has_automation_id = preg_match('/--automation-id=\S+/', join(" ", $argv)); if ($has_automation_id) { @@ -255,10 +194,6 @@ class Utils if (isset($options["h"]) || isset($options["help"])) { $help(); } else { - if (isset($options["log"])) { - self::log(true)->info("Logging to stdout enabled"); - } - $options = array_filter($options, function ($value) { return !is_string($value) || (!empty($value = trim($value)) && $value !== "__default_value"); @@ -268,7 +203,7 @@ class Utils if (is_array($config = $optionsByActionId($options["automation-id"]))) { $options = array_merge($config, $options); } else { - self::log()->error("No usable config found for automation-id {$options["automation-id"]}"); + LeUtils::log_error("No usable config found for automation-id {$options["automation-id"]}"); exit(1); } } @@ -277,7 +212,7 @@ class Utils $code = $runner($options); if ($code != $exit_success) { - self::log()->error("Command execution failed, exit code $code. Last input was: " . json_encode($options, JSON_UNESCAPED_SLASHES)); + LeUtils::log_error("Command execution failed, exit code $code. Last input was: " . json_encode($options, JSON_UNESCAPED_SLASHES)); } exit(isset($options["no-error"]) ? $exit_success : $code); @@ -290,7 +225,7 @@ class Utils $help(); } else { $cmd = join(" ", $argv); - self::log()->error("Parsing of '$cmd' failed at argument '{$argv[$index]}'"); + LeUtils::log_error("Parsing of '$cmd' failed at argument '{$argv[$index]}'"); } exit(1); } diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml index ab94015e5..9534d4bef 100644 --- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml @@ -1,15 +1,15 @@ //OPNsense/AcmeClient - 4.2.0 + 4.3.0 A secure ACME Client plugin - 0 + 0 Y - 1 + 1 Y @@ -28,32 +28,32 @@ N - prod + prod Production Environment [default] Staging Environment - 43580 + 43580 1024 65535 Y - 43581 + 43581 1024 65535 Y - 600 + 600 10 86400 Y - 0 + 0 N @@ -65,7 +65,7 @@ Related HAProxy ACL not found. - N + NN @@ -77,7 +77,7 @@ Related HAProxy action not found. - N + N N @@ -89,7 +89,7 @@ Related HAProxy server not found. - N + N N @@ -101,12 +101,12 @@ Related HAProxy backend not found. - N + N N Y - normal + normal normal extended @@ -117,7 +117,7 @@ Y - 1 + 1 @@ -126,17 +126,17 @@ N - 1 + 1 Y Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. @@ -144,7 +144,7 @@ Y - letsencrypt + letsencrypt Buypass Buypass Test CA @@ -159,17 +159,17 @@ N - /^https?:\/\/.*[^\/]$/ + /^https?:\/\/.*[^\/]$/ The URL must be a valid ACME endpoint without a trailing slash. N - /^.{1,8192}$/u + /^.{1,8192}$/u Should be a string between 1 and 8192 characters. N - /^.{1,8192}$/u + /^.{1,8192}$/u Should be a string between 1 and 8192 characters. @@ -179,7 +179,7 @@ N - 100 + 100 100 1000 @@ -195,23 +195,23 @@ N - 1 + 1 Y Y - /^[^\s^\t^,^;^\\^\/^(^)^\[^\]]{1,255}$/u + /^[^\s^\t^,^;^\\^\/^(^)^\[^\]]{1,255}$/u Please provide a valid FQDN, i.e. www.example.com or mail.example.com (max 255 characters). N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - Y - /^[^\s^\t^;^\\^\/^(^)^\[^\]]{1,65535}$/u + Y + /^[^\s^\t^;^\\^\/^(^)^\[^\]]{1,65535}$/u lower Please provide one or more valid FQDNs, i.e. www.example.com or mail.example.com. Field length is limited to 65535 characters. @@ -226,8 +226,8 @@ - Related item not found - N + Related item not found. + NY @@ -241,13 +241,13 @@ - Related item not found - N + Related item not found. + N Y Y - key_4096 + key_4096 2048 bit 3072 bit @@ -257,7 +257,7 @@ - 0 + 0 N @@ -271,24 +271,24 @@ - Related automation not found + Related automation not found. Y - Y + Y N - 1 + 1 Y Y 1 5000 - 60 + 60 Y - none + none Not using DNS alias mode Automatic Mode (uses DNS lookups) @@ -313,7 +313,7 @@ N - 100 + 100 100 1000 @@ -329,22 +329,22 @@ N - 1 + 1 Y Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. Y - dns01 + dns01 HTTP-01 DNS-01 @@ -353,14 +353,14 @@ Y - opnsense + opnsense OPNsense Web Service (automatic port forward) HAProxy HTTP Frontend Integration (OPNsense plugin) - 1 + 1 N @@ -371,10 +371,10 @@ N - Y + Y - 1 + 1 N @@ -389,19 +389,19 @@ - Related HAProxy frontend not found - Y + Related HAProxy frontend not found. + Y N Y - acme + acme acme.sh TLS Web Server (automatic port forward) - 1 + 1 N @@ -412,11 +412,11 @@ N - Y + Y Y - dns_freedns + dns_freedns 1984Hosting ACME DNS @@ -513,6 +513,7 @@ Rackspace rage4 RegRu + Scaleway SchlundTech selectel.com / selectel.ru Selfhost @@ -524,6 +525,7 @@ Variomedia.de Vscale Vultr + Websupport.sk World4You Yandex PDD Zilore @@ -534,7 +536,7 @@ 0 84600 - 0 + 0 Please specify a value between 0 and 84600 seconds. Y @@ -621,7 +623,7 @@ N - 1 + 1 N @@ -776,7 +778,7 @@ N - 1 + 1 N @@ -807,7 +809,7 @@ N - cloudflare + cloudflare Aliyun.com (UNSUPPORTED) AuroraDNS (UNSUPPORTED) @@ -889,7 +891,7 @@ N - https://api.loopia.se/RPCSERV + https://api.loopia.se/RPCSERV N @@ -993,11 +995,11 @@ N - localhost + localhost N - 443 + 443 N @@ -1007,7 +1009,7 @@ N - 0 + 0 N @@ -1195,7 +1197,7 @@ N - plain + plain plain SHA1 (deprecated in December 2022) @@ -1246,6 +1248,12 @@ N + + N + + + N + N @@ -1269,7 +1277,7 @@ N - https://identity.xxxx.conoha.io/v2.0 + https://identity.xxxx.conoha.io/v2.0 N @@ -1307,6 +1315,9 @@ N + + N + @@ -1315,17 +1326,17 @@ N - 1 + 1 Y Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. @@ -1334,10 +1345,12 @@ Restart OPNsense Web UI Restart HAProxy (OPNsense plugin) Restart Nginx (OPNsense plugin) + Reload Caddy (OPNsense plugin) Upload certificate via SFTP Remote Command via SSH Upload certificate to FRITZ!Box router Upload certificate to Palo Alto Networks Firewall + Upload certificate to Proxmox Backup Server Upload certificate to Proxmox VE Upload certificate to HashiCorp Vault Upload certificate to Synology DSM @@ -1348,26 +1361,26 @@ N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.+?\s(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?(?:\s.+?)?$/i + /^.+?\s(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?(?:\s.+?)?$/i Should be a valid public SSH host key (see "known_hosts"). N 1 65535 - 22 + 22 Should be a valid port number between 1 and 65535. N - /^.{1,128}$/u + /^.{1,128}$/u Should be a string between 1 and 128 characters. @@ -1380,70 +1393,74 @@ N - /^.{1,512}$/u + /^.{1,512}$/u Should be a string between 1 and 512 characters. N - /^[0-9]+$/u + /^[0-9]+$/u Should be a numeric value. N - /^0[0-9]{3}$/u + /^0[0-9]{3}$/u A unix permission, 4 digits (e.g. 0440). N - /^0[0-9]{3}$/u + /^0[0-9]{3}$/u A unix permission, 4 digits (e.g. 0400). + + N + 0 + N - /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui + /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui Should be a string between 1 and 255 characters. Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'. N - /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui + /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui Should be a string between 1 and 255 characters. Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'. N - /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui + /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui Should be a string between 1 and 255 characters. Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'. N - /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui + /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui Should be a string between 1 and 255 characters. Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.+?\s(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?(?:\s.+?)?$/i + /^.+?\s(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?(?:\s.+?)?$/i Should be a valid public SSH host key (see "known_hosts"). N 1 65535 - 22 + 22 Should be a valid port number between 1 and 65535. N - /^.{1,128}$/u + /^.{1,128}$/u Should be a string between 1 and 128 characters. @@ -1456,7 +1473,7 @@ N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a shell command between 1 and 1024 characters. @@ -1474,16 +1491,13 @@ Select a command from the list. N - - - N - + - 5000 + 5000 N - http + http N HTTP [default] @@ -1492,112 +1506,150 @@ N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - 1 + 1 N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - root + root N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - 8006 + 8006 N N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - pam + pam N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - acme + acme N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. + + root + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + 8007 + N + + + N + localhost + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + pam + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + acme + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - localhost + localhost N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - http + http N HTTP [default] @@ -1605,27 +1657,27 @@ - /usr/local/share/java/unifi/data/keystore + /usr/local/share/java/unifi/data/keystore N N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - acme + acme N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - 1 + 1 diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt index ec20a20e8..3836955d8 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt @@ -49,7 +49,6 @@ POSSIBILITY OF SUCH DAMAGE. ajax: true, selection: true, multiSelect: true, - rowCount:[10,25,50,100,500,1000], url: '/api/acmeclient/accounts/search', formatters: { "commands": function (column, row) { @@ -115,7 +114,9 @@ POSSIBILITY OF SUCH DAMAGE. /** * copy actions for selected items from opnsense_bootgrid_plugin.js */ - var grid_accounts = $("#grid-accounts").bootgrid(gridopt).on("loaded.rs.jquery.bootgrid", function (e) + const grid_accounts = $("#grid-accounts").UIBootgrid($.extend(gridParams, { options: gridopt })); + + $("#grid-accounts").on("loaded.rs.jquery.bootgrid", function (e) { // toggle all rendered tooltips (once for all) $('.bootgrid-tooltip').tooltip(); diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt index 9474a0613..2b57076f1 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt @@ -43,7 +43,6 @@ POSSIBILITY OF SUCH DAMAGE. del:'/api/acmeclient/actions/del/', toggle:'/api/acmeclient/actions/toggle/', options: { - rowCount:[10,25,50,100,500,1000] } } ); diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt index 7c6fa8ae7..e824edd85 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt @@ -56,7 +56,6 @@ POSSIBILITY OF SUCH DAMAGE. ajax: true, selection: true, multiSelect: true, - rowCount:[10,25,50,100,500,1000], url: '/api/acmeclient/certificates/search', formatters: { "commands": function (column, row) { @@ -140,7 +139,9 @@ POSSIBILITY OF SUCH DAMAGE. /** * copy actions for selected items from opnsense_bootgrid_plugin.js */ - var grid_certificates = $("#grid-certificates").bootgrid(gridopt).on("loaded.rs.jquery.bootgrid", function (e) + const grid_certificates = $("#grid-certificates").UIBootgrid($.extend(gridParams, { options: gridopt })); + + $("#grid_certificates").on("loaded.rs.jquery.bootgrid", function (e) { // toggle all rendered tooltips (once for all) $('.bootgrid-tooltip').tooltip(); @@ -330,7 +331,7 @@ POSSIBILITY OF SUCH DAMAGE. '{{ lang._('Forcefully issue or renew the selected certificate?') }}', '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() { // Handle HAProxy integration (no-op if not applicable) - ajaxCall(url="/api/acmeclient/settings/fetchHAProxyIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_ha_proxy_integration", sendData={}, callback=function(data,status) { ajaxCall(url=gridParams['sign'] + uuid,sendData={},callback=function(data,status){ // reload grid after sign $("#"+gridId).bootgrid("reload"); @@ -440,7 +441,7 @@ POSSIBILITY OF SUCH DAMAGE. $("#signallcertsAct").click(function(){ //$("#signallcertsAct_progress").addClass("fa fa-spinner fa-pulse"); // Handle HAProxy integration (no-op if not applicable) - ajaxCall(url="/api/acmeclient/settings/fetchHAProxyIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_ha_proxy_integration", sendData={}, callback=function(data,status) { ajaxCall(url="/api/acmeclient/service/signallcerts", sendData={}, callback=function(data,status) { // when done, disable progress animation. //$("#signallcertsAct_progress").removeClass("fa fa-spinner fa-pulse"); diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/logs.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/logs.volt index debf4ef22..dbfb34efb 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/logs.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/logs.volt @@ -35,7 +35,6 @@ sorting:false, rowSelect: false, selection: false, - rowCount:[20,50,100,200,500,1000,-1], requestHandler: function(request){ // Show only log entries that match 'AcmeClient' request['searchPhrase'] = 'acmeclient'; @@ -51,7 +50,6 @@ sorting:false, rowSelect: false, selection: false, - rowCount:[20,50,100,200,500,1000,-1], }, search:'/api/diagnostics/log/core/acmeclient' }); diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt index 287d8ad06..38e05c5e8 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt @@ -87,11 +87,11 @@ POSSIBILITY OF SUCH DAMAGE. }); // Handle cron integration - ajaxCall(url="/api/acmeclient/settings/fetchCronIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_cron_integration", sendData={}, callback=function(data,status) { }); // Handle HAProxy integration - ajaxCall(url="/api/acmeclient/settings/fetchHAProxyIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_ha_proxy_integration", sendData={}, callback=function(data,status) { }); // when done, disable progress animation @@ -128,9 +128,9 @@ POSSIBILITY OF SUCH DAMAGE. } // Handle cron integration - ajaxCall(url="/api/acmeclient/settings/fetchCronIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_cron_integration", sendData={}, callback=function(data,status) { // Handle HAProxy integration - ajaxCall(url="/api/acmeclient/settings/fetchHAProxyIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_ha_proxy_integration", sendData={}, callback=function(data,status) { // when done, disable progress animation $('[id*="reconfigureAct_progress"]').each(function(){ $(this).removeClass("fa fa-spinner fa-pulse"); diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt index c095c4298..f5643178b 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt @@ -43,7 +43,6 @@ POSSIBILITY OF SUCH DAMAGE. del:'/api/acmeclient/validations/del/', toggle:'/api/acmeclient/validations/toggle/', options: { - rowCount:[10,25,50,100,500,1000] } } ); @@ -57,13 +56,13 @@ POSSIBILITY OF SUCH DAMAGE. $("."+service_id).show(); } // Show a warning if the Google Cloud SDK plugin is missing. - ajaxCall(url="/api/acmeclient/settings/getGcloudPluginStatus", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/get_gcloud_plugin_status", sendData={}, callback=function(data,status) { if (data['result'] != 0) { $(".gcloud_plugin_warning").hide(); } }); // Show a warning if the BIND plugin is missing. - ajaxCall(url="/api/acmeclient/settings/getBindPluginStatus", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/get_bind_plugin_status", sendData={}, callback=function(data,status) { if (data['result'] != 0) { $(".bind_plugin_warning").hide(); } diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php index 019c44d81..5db3dd072 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php @@ -2,6 +2,7 @@ error("Failed getting identity. See log output for details."); + LeUtils::log_error("SSH failed getting identity. See log output for details."); } return EXITCODE_ERROR; } @@ -156,14 +158,14 @@ function commandTestConnection(array &$options): int function commandRunRemote(array &$options): int { if (empty($options["run"])) { - Utils::log()->error("SSH: Command is empty, nothing to do."); + LeUtils::log_error("SSH: Command is empty, nothing to do."); return EXITCODE_ERROR; } $lines = runRemoteCommand($options, $error); if (!$error) { $host = $options["host"] . (($port = ($options["port"] ?? false)) ? ":$port" : ""); - Utils::log()->info("SSH [$host]> {$options["run"]}:" . PHP_EOL . join(PHP_EOL, $lines)); + LeUtils::log_debug("SSH [$host]> {$options["run"]}:" . PHP_EOL . join(PHP_EOL, $lines)); return EXITCODE_SUCCESS; } @@ -244,7 +246,7 @@ function runRemoteCommand(array $options, &$error): ?array "exit_code" => $exit_code ]); $error["connect_failed"] = $exit_code == 255; - Utils::log()->error("SSH failed with '$exit_code': $cl", $error); + LeUtils::log_error("SSH failed with '$exit_code': $cl", $error); } return $result; @@ -253,7 +255,7 @@ function runRemoteCommand(array $options, &$error): ?array function buildSSHArguments(SSHKeys $ssh_keys, $host, $username, $identity_type = "", $host_key = "", $port = SSHKeys::DEFAULT_PORT): array { if (empty(trim($host)) || empty(trim($username))) { - Utils::log()->error("Failed connecting to '$host'. Hostname or username is missing."); + LeUtils::log_error("Failed connecting to '$host'. Hostname or username is missing."); return [false, ["invalid_parameters" => true]]; } @@ -263,7 +265,7 @@ function buildSSHArguments(SSHKeys $ssh_keys, $host, $username, $identity_type = $trust = $ssh_keys->trustHost($host, $host_key, $port); if ($trust["ok"] !== true) { - Utils::log()->error("Failed establishing trust in '$host'; Cause: {$trust["error"]}"); + LeUtils::log_error("Failed establishing trust in '$host'; Cause: {$trust["error"]}"); unset($trust["ok"]); return [false, array_merge($trust, ["host_not_trusted" => true])]; } else { @@ -288,7 +290,7 @@ function buildSSHArguments(SSHKeys $ssh_keys, $host, $username, $identity_type = "-oPreferredAuthentications=publickey" ); } else { - Utils::log()->error("Failed adding client identity ($identity). Connect will likely fail."); + LeUtils::log_error("Failed adding SSH client identity ($identity). Connect will likely fail."); } // Adding the host @@ -304,7 +306,7 @@ function help() function getOptionsById($automation_id) { - Utils::log()->info("Reading options from automation: $automation_id"); + LeUtils::log_debug("Reading options from automation: $automation_id"); if (is_object($action = Utils::getAutomationActionById($automation_id))) { if ($action->enabled && "configd_remote_ssh" === (string)$action->type) { @@ -317,10 +319,10 @@ function getOptionsById($automation_id) "run" => trim((string)$action->remote_ssh_command), ]; } else { - Utils::log()->error("Ignoring disabled or invalid automation '$automation_id'"); + LeUtils::log_error("Ignoring disabled or invalid automation '$automation_id'"); } } else { - Utils::log()->error("No upload automation found with uuid = '$automation_id'"); + LeUtils::log_error("No upload automation found with uuid = '$automation_id'"); } return false; diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh index 8a6352652..567364b40 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh @@ -20,7 +20,10 @@ chmod 750 ${ACME_BASE} ${ACME_BASE}/* # This should guard against manual misconfiguration. for link in ${ACME_LINKS}; do # First remove any existing file/directory. - if [ -f "${ACME_BASE}/home/${link}" ]; then + if [ -L "${ACME_BASE}/home/${link}" ]; then + # Already a symlink, skip this enty. + continue + elif [ -f "${ACME_BASE}/home/${link}" ]; then rm ${ACME_BASE}/home/${link} elif [ -d "${ACME_BASE}/home/${link}" ]; then rmdir ${ACME_BASE}/home/${link} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php index 2f42e46e5..b3510b909 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php @@ -142,6 +142,7 @@ use OPNsense\AcmeClient\SftpUploader; use OPNsense\AcmeClient\SftpClient; use OPNsense\AcmeClient\SSHKeys; use OPNsense\AcmeClient\Utils; +use OPNsense\AcmeClient\LeUtils; // Implementing logic function commandShowIdentity(array &$options): int @@ -162,7 +163,7 @@ function commandShowIdentity(array &$options): int echo file_get_contents($id_file); return EXITCODE_SUCCESS; } else { - Utils::log()->error("Failed getting identity. See log output for details."); + LeUtils::log_error("SFTP failed getting identity. See log output for details."); } return EXITCODE_ERROR; } @@ -214,7 +215,7 @@ function commandTestConnection(array &$options): int if ($remove_file) { if ($error = $sftp->clearError()->rm($filename)->lastError(3)) { - Utils::log()->error("Failed removing upload test file '$filename'", $error); + LeUtils::log_error("SFTP failed removing upload test file '$filename'", $error); } } @@ -261,7 +262,7 @@ function commandUpload(array &$options): int } elseif (isset($options["host"])) { return uploadCertificatesToHost($options); } else { - Utils::log()->error("No work to do, neither --host nor --certificates is present."); + LeUtils::log_error("No work to do, neither --host nor --certificates is present."); return EXITCODE_ERROR_NOTHING_TO_UPLOAD; } } @@ -270,7 +271,7 @@ function uploadCertificatesToHost(array $options): int { $sftp = connectWithServer($options, $error); if ($sftp === null) { - Utils::log()->error("Aborting after connect failure."); + LeUtils::log_error("SFTP aborting after connect failure."); return ($error["connect_failed"] ?? false) ? EXITCODE_ERROR : EXITCODE_ERROR_NO_PERMISSION; @@ -289,7 +290,7 @@ function uploadCertificatesToHost(array $options): int $result = $uploader->upload(); if ($result != SftpUploader::UPLOAD_SUCCESS) { - Utils::log()->error("Failed on " . json_encode($uploader->current(), JSON_UNESCAPED_SLASHES)); + LeUtils::log_error("SFTP failed on " . json_encode($uploader->current(), JSON_UNESCAPED_SLASHES)); switch ($result) { case SftpUploader::UPLOAD_ERROR_NO_PERMISSION: @@ -336,7 +337,7 @@ function connectWithServer(array $options, &$error): ?SftpClient if ($err = $sftp->cd($remote_path)->lastError()) { $error = $err; $error["change_home_dir_failed"] = true; - Utils::log()->error("Failed cd into '{$remote_path}'", $err); + LeUtils::log_error("SFTP failed cd into '{$remote_path}'", $err); return null; } } @@ -352,7 +353,7 @@ function help() function getOptionsById($automation_id, $silent = false) { if (!$silent) { - Utils::log()->info("Reading options from automation: $automation_id"); + LeUtils::log_debug("Reading options from automation: $automation_id"); } if (is_object($action = Utils::getAutomationActionById($automation_id))) { @@ -367,6 +368,7 @@ function getOptionsById($automation_id, $silent = false) "chgrp" => trim((string)$action->sftp_chgrp), "chmod" => trim((string)$action->sftp_chmod), "chmod-key" => trim((string)$action->sftp_chmod_key), + "modtime" => trim((string)$action->sftp_modtime), "cert-name" => trim((string)$action->sftp_filename_cert), "key-name" => trim((string)$action->sftp_filename_key), "ca-name" => trim((string)$action->sftp_filename_ca), @@ -374,10 +376,10 @@ function getOptionsById($automation_id, $silent = false) "certificates" => "", // defaults to all (= empty), may be overridden via CLI ]; } elseif (!$silent) { - Utils::log()->error("Ignoring disabled or invalid automation '$automation_id'"); + LeUtils::log_error("SFTP ignoring disabled or invalid automation '$automation_id'"); } } else { - Utils::log()->error("No upload automation found with uuid = '$automation_id'"); + LeUtils::log_error("No SFTP upload automation found with uuid = '$automation_id'"); } return false; @@ -388,19 +390,20 @@ function addFilesToUpload(array $options, SftpUploader &$uploader) $chmod = isset($options["chmod"]) ? ($options["chmod"] ?: DEFAULT_CERT_MODE) : false; $chmod_key = isset($options["chmod-key"]) ? ($options["chmod-key"] ?: DEFAULT_KEY_MODE) : false; $chgrp = ($options["chgrp"] ?? "") ?: false; + $modtime = ($options["modtime"] ?? "") ?: false; if (isset($options["certificates"])) { $cert_ids = preg_split('/[,;\s]+/', $options["certificates"] ?: "", 0, PREG_SPLIT_NO_EMPTY); foreach (findCertificates($cert_ids) as $cert) { if (!isset($cert["content"])) { - Utils::log()->error("Ignoring upload for cert '{$cert["name"]}', since it is not available in trust storage."); + LeUtils::log_error("Ignoring SFTP upload for cert '{$cert["name"]}', since it is not available in trust storage."); continue; } foreach ($cert["content"] as $name => $content) { if (empty($content)) { - Utils::log()->error("Content for '{$name}.pem' in cert '{$cert["name"]}' is empty, skipping it."); + LeUtils::log_error("Content for '{$name}.pem' in cert '{$cert["name"]}' is empty, skipping SFTP upload."); continue; } @@ -441,27 +444,27 @@ function addFilesToUpload(array $options, SftpUploader &$uploader) ? $chmod_key : $chmod; - $uploader->addContent($content, $target_path, $cert["updated"], $mod, $chgrp); + $uploader->addContent($content, $target_path, $cert["updated"], $mod, $chgrp, $modtime); } else { - Utils::log()->error("Cannot add '{$name}.pem' since the upload path '$target_path' is invalid."); + LeUtils::log_error("Cannot add '{$name}.pem' to SFTP upload since the upload path '$target_path' is invalid."); } } } if (empty($uploader->pending())) { - Utils::log()->error("Didn't find any certificates to upload (cert-ids: " . (empty($cert_ids) ? "*all*" : join(", ", $cert_ids)) . ")."); + LeUtils::log_error("Could not find any certificates for SFTP upload (cert-ids: " . (empty($cert_ids) ? "*all*" : join(", ", $cert_ids)) . ")."); } } elseif (isset($options["files"])) { $files = preg_split('/[,;\s]+/', $options["files"] ?: "", 0, PREG_SPLIT_NO_EMPTY); foreach ($files as $file) { - $uploader->addFile($file, "", $chmod, $chgrp); + $uploader->addFile($file, "", $chmod, $chgrp, $modtime); } if (empty($uploader->pending())) { - Utils::log()->error("Didn't files to upload (files: " . join(", ", $files) . ")."); + LeUtils::log_error("Could not find files for SFTP upload (files: " . join(", ", $files) . ")."); } } else { - Utils::log()->error("Neither '--certificates' nor '--files' was specified. Have nothing to upload."); + LeUtils::log_error("Neither '--certificates' nor '--files' was specified. Have nothing to upload."); } } @@ -489,7 +492,7 @@ function findCertificates(array $certificate_ids_or_names, $load_content = true) ) { if ($cert->enabled == 0) { if (!empty($certificate_ids_or_names)) { - Utils::log()->error("Certificate '{$name}' (id: $id) is disabled, skipping it."); + LeUtils::log_error("Certificate '{$name}' (id: $id) is disabled, skipping SFTP upload."); } continue; diff --git a/security/clamav/Makefile b/security/clamav/Makefile index f4d90f209..92c7a19be 100644 --- a/security/clamav/Makefile +++ b/security/clamav/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= clamav -PLUGIN_VERSION= 1.8 -PLUGIN_REVISION= 2 +PLUGIN_VERSION= 1.8.1 PLUGIN_COMMENT= Antivirus engine for detecting malicious threats PLUGIN_DEPENDS= clamav PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/security/clamav/pkg-descr b/security/clamav/pkg-descr index a601c42b3..d54ada582 100644 --- a/security/clamav/pkg-descr +++ b/security/clamav/pkg-descr @@ -9,6 +9,10 @@ database updates. Plugin Changelog ================ +1.8.1 + +* Fixed detect broken executables option (contributed by sopex) + 1.8 * Use syslog for logging diff --git a/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/forms/general.xml b/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/forms/general.xml index aa7841f34..d1cba308d 100644 --- a/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/forms/general.xml +++ b/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/forms/general.xml @@ -73,7 +73,7 @@ general.detectbroken checkbox - With this option clamav will try to detect broken executables (both PE and ELF) and mark them as Broken. + With this option ClamAV will alert on broken executables (both PE and ELF) and mark them as Broken. general.scanole2 diff --git a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/General.xml b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/General.xml index 4765e7b79..138ab9e83 100644 --- a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/General.xml +++ b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/General.xml @@ -4,143 +4,143 @@ 1.0.0 - 0 + 0 Y - 0 + 0 Y - 1 + 1 Y - 10 + 10 N - 100 + 100 N - 30 + 30 N - 20 + 20 N - 0 + 0 N - 0 + 0 N - 0 + 0 N - 1 + 1 N - 1 + 1 N - 0 + 0 N - 1 + 1 N - 0 + 0 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 0 + 0 N - 100M + 100M N - 25M + 25M N - 16 + 16 N - 10000 + 10000 N - 0 + 0 N - 0 + 0 N - database.clamav.net + database.clamav.net Y - 60 + 60 Y - 0 + 0 N - 0 + 0 N - 0 + 0 N - 0 + 0 N diff --git a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Url.xml b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Url.xml index 84447c3b9..0300219bf 100644 --- a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Url.xml +++ b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Url.xml @@ -6,7 +6,7 @@ - 1 + 1 Y @@ -14,7 +14,7 @@ Y - /^https?:\/\/.*$/i + /^https?:\/\/.*$/i URL has to start with http:// or https:// diff --git a/security/clamav/src/opnsense/mvc/app/views/OPNsense/ClamAV/general.volt b/security/clamav/src/opnsense/mvc/app/views/OPNsense/ClamAV/general.volt index c0765170e..a3cafb738 100644 --- a/security/clamav/src/opnsense/mvc/app/views/OPNsense/ClamAV/general.volt +++ b/security/clamav/src/opnsense/mvc/app/views/OPNsense/ClamAV/general.volt @@ -112,12 +112,12 @@ $( document ).ready(function() { }); $("#grid-lists").UIBootgrid( - { 'search':'/api/clamav/url/searchUrl', - 'get':'/api/clamav/url/getUrl/', - 'set':'/api/clamav/url/setUrl/', - 'add':'/api/clamav/url/addUrl/', - 'del':'/api/clamav/url/delUrl/', - 'toggle':'/api/clamav/url/toggleUrl/' + { 'search':'/api/clamav/url/search_url', + 'get':'/api/clamav/url/get_url/', + 'set':'/api/clamav/url/set_url/', + 'add':'/api/clamav/url/add_url/', + 'del':'/api/clamav/url/del_url/', + 'toggle':'/api/clamav/url/toggle_url/' } ); diff --git a/security/clamav/src/opnsense/service/conf/actions.d/actions_clamav.conf b/security/clamav/src/opnsense/service/conf/actions.d/actions_clamav.conf index 59ad71ea7..2e20af3ab 100644 --- a/security/clamav/src/opnsense/service/conf/actions.d/actions_clamav.conf +++ b/security/clamav/src/opnsense/service/conf/actions.d/actions_clamav.conf @@ -20,6 +20,7 @@ command: /usr/local/etc/rc.d/clamav_clamd restart parameters: type:script +description:Restart ClamAV message:restarting ClamAV [status] diff --git a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamd.conf b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamd.conf index 88778b060..7116c5428 100644 --- a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamd.conf +++ b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamd.conf @@ -38,7 +38,7 @@ ScanPE yes ScanELF yes {% endif %} {% if helpers.exists('OPNsense.clamav.general.detectbroken') and OPNsense.clamav.general.detectbroken == '1' %} -DetectBrokenExecutables yes +AlertBrokenExecutables yes {% endif %} {% if helpers.exists('OPNsense.clamav.general.scanole2') and OPNsense.clamav.general.scanole2 == '1' %} ScanOLE2 yes diff --git a/security/crowdsec/+POST_DEINSTALL.post b/security/crowdsec/+POST_DEINSTALL.post index 536a32529..5934e52d4 100755 --- a/security/crowdsec/+POST_DEINSTALL.post +++ b/security/crowdsec/+POST_DEINSTALL.post @@ -41,8 +41,8 @@ function removeAlias($name) } } -removeAlias('crowdsec_blacklists'); -removeAlias('crowdsec6_blacklists'); +removeAlias('crowdsec_blocklists'); +removeAlias('crowdsec6_blocklists'); EOT diff --git a/security/crowdsec/Makefile b/security/crowdsec/Makefile index dad0bf60c..1d6d54f33 100644 --- a/security/crowdsec/Makefile +++ b/security/crowdsec/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= crowdsec -PLUGIN_VERSION= 1.0.8 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.0.12 PLUGIN_DEPENDS= crowdsec PLUGIN_COMMENT= Lightweight and collaborative security engine PLUGIN_MAINTAINER= marco@crowdsec.net diff --git a/security/crowdsec/pkg-descr b/security/crowdsec/pkg-descr index cb1429cf4..6547b151f 100644 --- a/security/crowdsec/pkg-descr +++ b/security/crowdsec/pkg-descr @@ -8,6 +8,29 @@ WWW: https://crowdsec.net/ Plugin Changelog ================ +1.0.12 + + * Fix and update service management (start/stop/reload/configure) + +1.0.11 + +* convert tables to UIBootGrid (required for opnsense 25.7) +* separate page for each table +* IPv6 validation for LAPI listen address broken (contributed by BPplays) +* Fix alert time not showing in grid + +1.0.10 + +* changed alias names crowdsec*blacklists -> crowdsec*blocklists +* added rules for outgoing connections too +* added enroll_key to settings for automatic enrollment +* option to disable rule generation (bring your own rules!) +* code cleanup, reformat, typing + +1.0.9 + +* Update rule reference ($ -> <>) for opnsense 25.1 + 1.0.8 * Enable use_wal, remove warning @@ -23,7 +46,7 @@ Plugin Changelog 1.0.6 - * default acquis.d/opnsense.yaml to "poll_without_inotify=true" which is now required +* default acquis.d/opnsense.yaml to "poll_without_inotify=true" which is now required to acquire content from symlinks. 1.0.5 diff --git a/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc b/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc index 386bc73ca..b226f424a 100644 --- a/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc +++ b/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc @@ -43,39 +43,69 @@ function crowdsec_firewall(Plugin $fw) $rules_tag = $general['rules_tag']; } - add_alias_if_not_exist('crowdsec_blacklists', 'CrowdSec (IPv4)', 'IPv4'); + add_alias_if_not_exist('crowdsec_blocklists', 'CrowdSec (IPv4)', 'IPv4'); + add_alias_if_not_exist('crowdsec6_blocklists', 'CrowdSec (IPv6)', 'IPv6'); // https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/library/OPNsense/Firewall/FilterRule.php - $fw->registerFilterRule( - 1, /* priority */ - array( - 'ipprotocol' => 'inet', - 'descr' => 'CrowdSec (IPv4)', - 'from' => '$crowdsec_blacklists', # $ to reference an alias - 'direction' => 'in', - 'type' => 'block', - 'log' => $rules_log_enabled, - 'tag' => $rules_tag, - 'quick' => true - ) - ); + // if missing, default to true + if (!isset($general['rules_enabled']) || $general['rules_enabled'] != 0) { + $fw->registerFilterRule( + 1, /* priority */ + array( + 'ipprotocol' => 'inet', + 'descr' => 'CrowdSec (IPv4) in', + 'from' => '', + 'direction' => 'in', + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'quick' => true + ) + ); - add_alias_if_not_exist('crowdsec6_blacklists', 'CrowdSec (IPv6)', 'IPv6'); + $fw->registerFilterRule( + 1, /* priority */ + array( + 'ipprotocol' => 'inet', + 'descr' => 'CrowdSec (IPv4) out', + 'to' => '', + 'direction' => 'out', + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'quick' => true + ) + ); - $fw->registerFilterRule( - 1, /* priority */ - array( - 'ipprotocol' => 'inet6', - 'descr' => 'CrowdSec (IPv6)', - 'from' => '$crowdsec6_blacklists', # $ to reference an alias - 'direction' => 'in', - 'type' => 'block', - 'log' => $rules_log_enabled, - 'tag' => $rules_tag, - 'quick' => true - ) - ); + $fw->registerFilterRule( + 1, /* priority */ + array( + 'ipprotocol' => 'inet6', + 'descr' => 'CrowdSec (IPv6) in', + 'from' => '', + 'direction' => 'in', + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'quick' => true + ) + ); + + $fw->registerFilterRule( + 1, /* priority */ + array( + 'ipprotocol' => 'inet6', + 'descr' => 'CrowdSec (IPv6) out', + 'to' => '', + 'direction' => 'out', + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'quick' => true + ) + ); + } } function crowdsec_services() diff --git a/security/crowdsec/src/etc/rc.d/oscrowdsec b/security/crowdsec/src/etc/rc.d/oscrowdsec index 0d310efd6..87a34c703 100755 --- a/security/crowdsec/src/etc/rc.d/oscrowdsec +++ b/security/crowdsec/src/etc/rc.d/oscrowdsec @@ -53,6 +53,10 @@ oscrowdsec_status () { service crowdsec status ret=$? + if ! service crowdsec_firewall enabled; then + return $ret + fi + if ! service crowdsec_firewall status; then ret=1 fi diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AlertsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AlertsController.php new file mode 100644 index 000000000..b37b60660 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AlertsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class AlertsController + * @package OPNsense\CrowdSec + */ +class AlertsController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/alerts'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php index 6aff27e81..ae648a793 100644 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php @@ -6,7 +6,6 @@ namespace OPNsense\CrowdSec\Api; use OPNsense\Base\ApiControllerBase; -use OPNsense\CrowdSec\CrowdSec; use OPNsense\Core\Backend; /** @@ -15,19 +14,75 @@ use OPNsense\Core\Backend; class AlertsController extends ApiControllerBase { /** - * retrieve list of alerts + * Format scope and value as "scope:value" + * + * @param array $source Array with 'scope' and 'value' keys (can be a decision) + * @return string Formatted string + */ + private function formatScopeValue(array $source): string + { + $scope = $source['scope'] ?? ''; + if ($source['value'] !== '') { + $scope = $scope . ':' . $source['value']; + } + return $scope; + } + + /** + * Summarize decision types as "type1:count1 type2:count2 ..." + * + * @param array $decisions List of decision arrays + * @return string Summary string + */ + private function formatDecisions(array $decisions): string + { + $counts = []; + + foreach ($decisions as $decision) { + if (!isset($decision['type'])) { + continue; + } + + $type = $decision['type']; + $counts[$type] = ($counts[$type] ?? 0) + 1; + } + + $parts = []; + foreach ($counts as $type => $count) { + $parts[] = "{$type}:{$count}"; + } + + return implode(' ', $parts); + } + + /** + * Retrieve list of alerts + * * @return array of alerts * @throws \OPNsense\Base\ModelException * @throws \ReflectionException */ - public function getAction() + public function searchAction(): array { - $backend = new Backend(); - $bckresult = json_decode(trim($backend->configdRun("crowdsec alerts-list")), true); - if ($bckresult !== null) { - // only return valid json type responses - return $bckresult; + $result = json_decode(trim((new Backend())->configdRun("crowdsec alerts-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; } - return array("message" => "unable to list alerts"); + + $rows = []; + foreach ($result as $alert) { + $source = $alert['source'] ?? []; + $rows[] = [ + 'id' => $alert['id'], + 'value' => $this->formatScopeValue($source ?? []), + 'reason' => $alert['scenario'] ?? '', + 'country' => $source['cn'] ?? '', + 'as' => $source['as_name'] ?? '', + 'decisions' => $this->formatDecisions($alert['decisions'] ?? []), + 'created' => $alert['created_at'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); } } diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecconfigsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecconfigsController.php new file mode 100644 index 000000000..320339f86 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecconfigsController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class AppsecconfigsController extends ApiControllerBase +{ + /** + * Retrieve the installed appsec-configs + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec appsec-configs-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["appsec-configs"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecrulesController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecrulesController.php new file mode 100644 index 000000000..583b58baf --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecrulesController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class AppsecrulesController extends ApiControllerBase +{ + /** + * Retrieve the installed appsec-rules + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec appsec-rules-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["appsec-rules"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php index 94de1a877..ecaadbea4 100644 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php @@ -6,7 +6,6 @@ namespace OPNsense\CrowdSec\Api; use OPNsense\Base\ApiControllerBase; -use OPNsense\CrowdSec\CrowdSec; use OPNsense\Core\Backend; /** @@ -15,19 +14,33 @@ use OPNsense\Core\Backend; class BouncersController extends ApiControllerBase { /** - * retrieve list of bouncers + * Retrieve list of bouncers + * * @return array of bouncers * @throws \OPNsense\Base\ModelException * @throws \ReflectionException */ - public function getAction() + public function searchAction(): array { - $backend = new Backend(); - $bckresult = json_decode(trim($backend->configdRun("crowdsec bouncers-list")), true); - if ($bckresult !== null) { - // only return valid json type responses - return $bckresult; + $result = json_decode(trim((new Backend())->configdRun("crowdsec bouncers-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; } - return array("message" => "unable to list bouncers"); + + $rows = []; + foreach ($result as $bouncer) { + $rows[] = [ + 'name' => $bouncer['name'], + 'type' => $bouncer['type'] ?? '', + 'version' => $bouncer['version'] ?? '', + 'created' => $bouncer['created_at'] ?? '', + 'valid' => ($bouncer['revoked'] ?? false) !== true, + 'ip_address' => $bouncer['ip_address'] ?? '', + 'last_seen' => $bouncer['last_pull'] ?? '', + 'os' => $bouncer['os'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); } } diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php new file mode 100644 index 000000000..40449fdee --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class CollectionsController extends ApiControllerBase +{ + /** + * Retrieve the installed collections + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec collections-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["collections"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php index 7421e74e4..99c3fbb53 100644 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php @@ -6,45 +6,112 @@ namespace OPNsense\CrowdSec\Api; use OPNsense\Base\ApiControllerBase; -use OPNsense\CrowdSec\CrowdSec; use OPNsense\Core\Backend; +function unrollDecisions(array $alerts): array +{ + $result = []; + + foreach ($alerts as $alert) { + if (!isset($alert['decisions']) || !is_array($alert['decisions'])) { + continue; + } + + foreach ($alert['decisions'] as $decision) { + // ignore deleted decisions + if (isset($decision['duration']) && str_starts_with($decision['duration'], '-')) { + continue; + } + + $row = $decision; + + // Add parent alert fields with prefix + foreach ($alert as $key => $value) { + if ($key === 'decisions') { + continue; // skip nested array + } + $row["alert_" . $key] = $value; + } + + $result[] = $row; + } + } + + return $result; +} + + /** * @package OPNsense\CrowdSec */ class DecisionsController extends ApiControllerBase { /** - * retrieve list of decisions + * Format scope and value as "scope:value" + * + * @param array $source Array with 'scope' and 'value' keys + * @return string Formatted string + */ + private function formatScopeValue(array $source): string + { + $scope = $source['scope'] ?? ''; + if ($source['value'] !== '') { + $scope = $scope . ':' . $source['value']; + } + return $scope; + } + + /** + * Retrieve list of decisions + * * @return array of decisions * @throws \OPNsense\Base\ModelException * @throws \ReflectionException */ - public function getAction() + public function searchAction(): array { - $backend = new Backend(); - $bckresult = json_decode(trim($backend->configdRun("crowdsec decisions-list")), true); - if ($bckresult !== null) { - // only return valid json type responses - return $bckresult; + $result = json_decode(trim((new Backend())->configdRun("crowdsec decisions-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; } - return array("message" => "unable to list decisions"); + + $decisions = unrollDecisions($result); + + $rows = []; + foreach ($decisions as $dec) { + $alert_source = $dec['alert_source'] ?? []; + + $rows[] = [ + 'id' => $dec['id'], + 'source' => $dec['origin'] ?? '', + 'scope_value' => $this->formatScopeValue($dec), + 'reason' => $dec['scenario'] ?? '', + 'action' => $dec['type'] ?? '', + 'country' => $alert_source['cn'] ?? '', + 'as' => $alert_source['as_name'] ?? '', + 'events_count' => $dec['alert_events_count'] ?? '', + 'expiration' => $dec['duration'] ?? '', + 'alert_id' => $dec['alert_id'], + ]; + } + + return $this->searchRecordsetBase($rows); } - public function deleteAction($decision_id) + public function delAction($decision_id): array { - if ($this->request->isDelete()) { - $backend = new Backend(); - $bckresult = $backend->configdRun("crowdsec decisions-delete ${decision_id}"); - if ($bckresult !== null) { - // why does the action return \n\n for empty output? - if (trim($bckresult) === '') { - return array("message" => "OK"); - } - // TODO handle error - return array("message" => $bckresult); + if ($this->request->isPost()) { + $result = (new Backend())->configdRun("crowdsec decisions-delete {$decision_id}"); + if ($result === null) { + return ["result" => "deleted"]; } - return array("message" => "OK"); + + // why does the action return \n\n for empty output? + if (trim($result) === '') { + return ["result" => "deleted"]; + } + // TODO assume not found, should handle other errors + return ["result" => "not found"]; } else { $this->response->setStatusCode(405, "Method Not Allowed"); $this->response->setHeader("Allow", "DELETE"); diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/HubController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/HubController.php deleted file mode 100644 index a8114ff2c..000000000 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/HubController.php +++ /dev/null @@ -1,33 +0,0 @@ - - -namespace OPNsense\CrowdSec\Api; - -use OPNsense\Base\ApiControllerBase; -use OPNsense\CrowdSec\CrowdSec; -use OPNsense\Core\Backend; - -/** - * @package OPNsense\CrowdSec - */ -class HubController extends ApiControllerBase -{ - /** - * retrieve the registered hub items - * @return dictionary of items, by type - * @throws \OPNsense\Base\ModelException - * @throws \ReflectionException - */ - public function getAction() - { - $backend = new Backend(); - $bckresult = json_decode(trim($backend->configdRun("crowdsec hub-items")), true); - if ($bckresult !== null) { - // only return valid json type responses - return $bckresult; - } - return array("message" => "unable to list hub items"); - } -} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php index 617e43bf4..714a5c252 100644 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php @@ -6,7 +6,6 @@ namespace OPNsense\CrowdSec\Api; use OPNsense\Base\ApiControllerBase; -use OPNsense\CrowdSec\CrowdSec; use OPNsense\Core\Backend; /** @@ -15,19 +14,32 @@ use OPNsense\Core\Backend; class MachinesController extends ApiControllerBase { /** - * retrieve list of registered machines + * Retrieve list of machines + * * @return array of machines * @throws \OPNsense\Base\ModelException * @throws \ReflectionException */ - public function getAction() + public function searchAction(): array { - $backend = new Backend(); - $bckresult = json_decode(trim($backend->configdRun("crowdsec machines-list")), true); - if ($bckresult !== null) { - // only return valid json type responses - return $bckresult; + $result = json_decode(trim((new Backend())->configdRun("crowdsec machines-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; } - return array("message" => "unable to list machines"); + + $rows = []; + foreach ($result as $machine) { + $rows[] = [ + 'name' => $machine['machineId'], + 'ip_address' => $machine['ipAddress'] ?? '', + 'version' => $machine['version'] ?? '', + 'validated' => $machine['isValidated'] ?? false, + 'created' => $machine['created_at'] ?? '', + 'last_seen' => $machine['last_heartbeat'] ?? '', + 'os' => $machine['os'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); } } diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php new file mode 100644 index 000000000..7a904e66a --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class ParsersController extends ApiControllerBase +{ + /** + * Retrieve the installed parsers + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec parsers-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["parsers"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php new file mode 100644 index 000000000..4a9ea031f --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class PostoverflowsController extends ApiControllerBase +{ + /** + * Retrieve the installed postoverflows + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec postoverflows-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["postoverflows"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php new file mode 100644 index 000000000..4c1244576 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class ScenariosController extends ApiControllerBase +{ + /** + * Retrieve the installed scenarios + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec scenarios-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["scenarios"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php index 477400418..c18ebb029 100644 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php @@ -5,74 +5,41 @@ namespace OPNsense\CrowdSec\Api; -use OPNsense\Base\ApiControllerBase; +use OPNsense\Base\ApiMutableServiceControllerBase; use OPNsense\Core\Backend; /** * Class ServiceController * @package OPNsense\CrowdSec */ -class ServiceController extends ApiControllerBase +class ServiceController extends ApiMutableServiceControllerBase { - /** - * reconfigure CrowdSec - */ - public function reloadAction() + protected static $internalServiceClass = '\OPNsense\CrowdSec\General'; + protected static $internalServiceTemplate = 'OPNsense/CrowdSec'; + protected static $internalServiceName = 'crowdsec'; + + protected function ServiceEnabled() { - $status = "failed"; - if ($this->request->isPost()) { - $backend = new Backend(); - $bckresult = trim($backend->configdRun('template reload OPNsense/CrowdSec')); - if ($bckresult == "OK") { - $bckresult = trim($backend->configdRun('crowdsec reconfigure')); - if ($bckresult == "OK") { - $status = "ok"; - } - } - } - return array("status" => $status); - } + $mdl = $this->getModel(); - /** - * retrieve status of crowdsec - * @return array - * @throws \Exception - */ - public function statusAction() - { - $backend = new Backend(); - $response = $backend->configdRun("crowdsec crowdsec-status"); - - $status = "unknown"; - if (strpos($response, "not running") > 0) { - $status = "stopped"; - } elseif (strpos($response, "is running") > 0) { - $status = "running"; - } - - $response = $backend->configdRun("crowdsec crowdsec-firewall-status"); - - $firewall_status = "unknown"; - if (strpos($response, "not running") > 0) { - $firewall_status = "stopped"; - } elseif (strpos($response, "is running") > 0) { - $firewall_status = "running"; - } - - return array( - "crowdsec-status" => $status, - "crowdsec-firewall-status" => $firewall_status, + return ( + $mdl->agent_enabled->__toString() === "1" || + $mdl->lapi_enabled->__toString() === "1" || + $mdl->firewall_bouncer_enabled->__toString() === "1" ); } - /** - * return debug information - * @return array - */ - public function debugAction() + public function reconfigureAction() { - $backend = new Backend(); - $response = $backend->configdRun("crowdsec debug"); - return array("message" => $response); + // Run the default reconfigure logic + $result = parent::reconfigureAction(); + + // Now we generate the config.yaml and config-firewall-bouncer.yaml files + if (isset($result['status']) && $result['status'] === 'ok') { + $backend = new Backend(); + $backend->configdRun('crowdsec reconfigure'); + } + + return $result; } } diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php index d236c26f8..cea95d0d7 100644 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php @@ -6,7 +6,6 @@ namespace OPNsense\CrowdSec\Api; use OPNsense\Base\ApiControllerBase; -use OPNsense\CrowdSec\CrowdSec; use OPNsense\Core\Backend; /** @@ -15,14 +14,14 @@ use OPNsense\Core\Backend; class VersionController extends ApiControllerBase { /** - * retrieve version description + * Retrieve version description + * * @return version description * @throws \OPNsense\Base\ModelException * @throws \ReflectionException */ - public function getAction() + public function getAction(): string { - $backend = new Backend(); - return $backend->configdRun("crowdsec version"); + return (new Backend())->configdRun("crowdsec version"); } } diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecconfigsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecconfigsController.php new file mode 100644 index 000000000..d63e2f6a7 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecconfigsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class AppsecconfigsController + * @package OPNsense\CrowdSec + */ +class AppsecconfigsController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/appsecconfigs'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecrulesController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecrulesController.php new file mode 100644 index 000000000..ad06ba30a --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecrulesController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class AppsecrulesController + * @package OPNsense\CrowdSec + */ +class AppsecrulesController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/appsecrules'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/BouncersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/BouncersController.php new file mode 100644 index 000000000..beb79086d --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/BouncersController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class BouncersController + * @package OPNsense\CrowdSec + */ +class BouncersController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/bouncers'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/CollectionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/CollectionsController.php new file mode 100644 index 000000000..814c1ebe6 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/CollectionsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class CollectionsController + * @package OPNsense\CrowdSec + */ +class CollectionsController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/collections'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/DecisionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/DecisionsController.php new file mode 100644 index 000000000..6cd08cc03 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/DecisionsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class DecisionsController + * @package OPNsense\CrowdSec + */ +class DecisionsController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/decisions'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php index 115ca686c..fc3ea58a3 100644 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php @@ -11,7 +11,7 @@ namespace OPNsense\CrowdSec; */ class GeneralController extends \OPNsense\Base\IndexController { - public function indexAction() + public function indexAction(): void { $this->view->pick('OPNsense/CrowdSec/general'); $this->view->generalForm = $this->getForm("general"); diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/MachinesController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/MachinesController.php new file mode 100644 index 000000000..efa867450 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/MachinesController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class MachinesController + * @package OPNsense\CrowdSec + */ +class MachinesController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/machines'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php index 6dbd461d5..f15ca5218 100644 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php @@ -11,7 +11,7 @@ namespace OPNsense\CrowdSec; */ class OverviewController extends \OPNsense\Base\IndexController { - public function indexAction() + public function indexAction(): void { $this->view->pick('OPNsense/CrowdSec/overview'); } diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ParsersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ParsersController.php new file mode 100644 index 000000000..adb1c6c14 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ParsersController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class ParsersController + * @package OPNsense\CrowdSec + */ +class ParsersController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/parsers'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/PostoverflowsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/PostoverflowsController.php new file mode 100644 index 000000000..d20de2e82 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/PostoverflowsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class PostoverflowsController + * @package OPNsense\CrowdSec + */ +class PostoverflowsController extends \OPNsense\Base\IndexController +{ + public function indexAction() + { + $this->view->pick('OPNsense/CrowdSec/postoverflows'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ScenariosController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ScenariosController.php new file mode 100644 index 000000000..d1bdbd4a0 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ScenariosController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class ScenariosController + * @package OPNsense\CrowdSec + */ +class ScenariosController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/scenarios'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml index 45d9d3f24..c4480f9f7 100644 --- a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml @@ -27,6 +27,14 @@ packets from the attacking IP addresses. + + + general.enroll_key + + text + Click "Enroll command" on the the website and copy the key here. + + general.lapi_manual_configuration @@ -66,6 +74,16 @@ services. + + + general.rules_enabled + + checkbox + Generate block rules from the Crowdsec blocklists. + They are applied to all interfaces, ipv4/v6, ingress and egress. + If you disable this, you'll have to write your own rules to block anything. + + general.rules_log diff --git a/security/crowdsec/src/opnsense/mvc/app/library/OPNsense/CrowdSec/Util.php b/security/crowdsec/src/opnsense/mvc/app/library/OPNsense/CrowdSec/Util.php new file mode 100644 index 000000000..e0eb5a0db --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/library/OPNsense/CrowdSec/Util.php @@ -0,0 +1,15 @@ + //OPNsense/crowdsec/general CrowdSec general configuration - 1.0.8 + 1.0.12 - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 0 + 0 Y - - 127.0.0.1 + + 127.0.0.1 Y - ((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$)) + N - 8080 + 8080 Y N N + + 1 + Y + + - 0 + 0 Y @@ -47,8 +52,13 @@ A tag must only contain numbers and letters and must be between 1 and 63 characters. + + /^([0-9a-zA-Z]{1,63})$/u + The enrollment key can only contain numbers and letters and must be between 1 and 63 characters. Did you take it from app.crowdsec.net? + + - 0 + 0 Y diff --git a/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml index c50b8cd43..99e4d4e33 100644 --- a/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml +++ b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml @@ -2,7 +2,16 @@ - + + + + + + + + + + diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/alerts.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/alerts.volt new file mode 100644 index 000000000..9ad6bc6e2 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/alerts.volt @@ -0,0 +1,62 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + +
        {{ lang._('Enabled') }}{{ lang._('Network Id') }}{{ lang._('Local Description') }}{{ lang._('Commands') }}{{ lang._('Enabled') }}{{ lang._('Network Id') }}{{ lang._('Local Description') }}{{ lang._('Commands') }} {{ lang._('ID') }}
        + + + + + + + + + + + + + + + +
        IDValueReasonCountryASDecisionsCreated
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecconfigs.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecconfigs.volt new file mode 100644 index 000000000..53dcfd0ae --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecconfigs.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
        NameStatusVersionPathDescription
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecrules.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecrules.volt new file mode 100644 index 000000000..8e6ce811b --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecrules.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
        NameStatusVersionPathDescription
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/bouncers.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/bouncers.volt new file mode 100644 index 000000000..43d4e0e7f --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/bouncers.volt @@ -0,0 +1,44 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + + + + +
        NameTypeVersionCreatedValidIP AddressLast SeenOS
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/collections.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/collections.volt new file mode 100644 index 000000000..ca85d2e7b --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/collections.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
        NameStatusVersionPathDescription
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/decisions.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/decisions.volt new file mode 100644 index 000000000..0eb4bc197 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/decisions.volt @@ -0,0 +1,51 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + +Note: the decisions coming from the CAPI (signals collected by the CrowdSec users) do not appear here. +To show them, use cscli decisions list -a in a shell. + + + + + + + + + + + + + + + + + + + + + + + + +
        IDSourceScope:ValueReasonActionCountryASEventsExpirationAlert IDCommands
        + + +
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt index c7ae96adf..95b257727 100644 --- a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt @@ -3,7 +3,7 @@ + + + + + + + + + + + + + + + + + + + +
        NameVersionValidated?IP AddressCreatedLast SeenOS
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/overview.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/overview.volt deleted file mode 100644 index a51cbb5bf..000000000 --- a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/overview.volt +++ /dev/null @@ -1,252 +0,0 @@ -{# SPDX-License-Identifier: MIT #} -{# SPDX-FileCopyrightText: © 2021 CrowdSec #} - - - - - - - - -
        - Service status: crowdsec ... - firewall bouncer ... -
        - - - -
        - -
        - - - - - - - - - - - - - - - - -
        NameIP AddressLast UpdateValidated?Version
        -
        - -
        - - - - - - - - - - - - - - - - - -
        NameIP AddressValidLast API PullTypeVersion
        -
        - -
        - - - - - - - - - - - - - - - - -
        CollectionStatusVersionPathDescription
        -
        - -
        - - - - - - - - - - - - - - - - -
        ScenarioStatusVersionPathDescription
        -
        - -
        - - - - - - - - - - - - - - - - -
        ParserStatusVersionPathDescription
        -
        - -
        - - - - - - - - - - - - - - - - -
        PostoverflowStatusVersionPathDescription
        -
        - -
        - - - - - - - - - - - - - - - - - - -
        IDValueReasonCountryASDecisionsCreated At
        -
        - -
        - Note: the decisions coming from the CAPI (signals collected by the CrowdSec users) do not appear here. - To show them, use cscli decisions list -a in a shell. - - - - - - - - - - - - - - - - - - - - - - -
        IDSourceScope:ValueReasonActionCountryASEventsExpirationAlert ID
        -
        - -
        -
        -        
        -
        - - - - -
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/parsers.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/parsers.volt new file mode 100644 index 000000000..0ae9c2576 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/parsers.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
        NameStatusVersionPathDescription
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/postoverflows.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/postoverflows.volt new file mode 100644 index 000000000..9008ff224 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/postoverflows.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
        NameStatusVersionPathDescription
        diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/scenarios.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/scenarios.volt new file mode 100644 index 000000000..a42cd7a62 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/scenarios.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
        NameStatusVersionPathDescription
        diff --git a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh index 50b81c744..2b6d7731b 100755 --- a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh +++ b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh @@ -2,9 +2,9 @@ test -x /usr/local/bin/cscli || exit 0 -/usr/local/bin/cscli --error hub update +/usr/local/bin/cscli --error -o human hub update >/dev/null -upgraded=$(/usr/local/bin/cscli --error hub upgrade) +upgraded=$(/usr/local/bin/cscli --error -o human hub upgrade) if [ ! -e "/usr/local/etc/crowdsec/collections/opnsense.yaml" ]; then /usr/local/bin/cscli --error collections install crowdsecurity/opnsense diff --git a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py index 31fb26643..6d4869c5b 100755 --- a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py +++ b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py @@ -2,33 +2,41 @@ import logging import json +import subprocess import urllib.parse +from typing import cast, Any import yaml logging.basicConfig(level=logging.INFO) -def load_config(filename): +def is_ipv6(ip: str) -> bool: + return ":" in ip + + +def load_config(filename: str) -> dict[str, Any]: with open(filename) as fin: return yaml.safe_load(fin) # only save if some value has changed -def save_config(filename, new_config): +def save_config(filename: str, new_config: dict[str, Any]): old_config = load_config(filename) if old_config != new_config: with open(filename, 'w') as fout: yaml.dump(new_config, fout) -def get_netloc(settings): +def get_netloc(settings: dict[str, str]): # defaults if config has not been saved yet listen_address = settings.get('lapi_listen_address', '127.0.0.1') listen_port = settings.get('lapi_listen_port', '8080') + if is_ipv6(listen_address): + listen_address = '[{}]'.format(listen_address) return '{}:{}'.format(listen_address, listen_port) -def get_new_url(old_url, settings): +def get_new_url(old_url: str, settings: dict[str, str]): old_tuple = urllib.parse.urlsplit(old_url) new_tuple = old_tuple._replace(netloc=get_netloc(settings)) new_url = urllib.parse.urlunsplit(new_tuple) @@ -39,7 +47,7 @@ def get_new_url(old_url, settings): return new_url -def configure_agent(settings): +def configure_agent(settings: dict[str, str]): config_path = '/usr/local/etc/crowdsec/config.yaml' config = load_config(config_path) @@ -47,13 +55,16 @@ def configure_agent(settings): config['crowdsec_service']['acquisition_dir'] = '/usr/local/etc/crowdsec/acquis.d/' config['db_config']['use_wal'] = True + enable = int(settings.get('agent_enabled', '0')) + config['crowdsec_service']['enable'] = bool(enable) + if not int(settings.get('lapi_manual_configuration', '0')): config['api']['server']['listen_uri'] = get_netloc(settings) save_config(config_path, config) -def configure_lapi(settings): +def configure_lapi(settings: dict[str, str]): config_path = '/usr/local/etc/crowdsec/config.yaml' config = load_config(config_path) @@ -63,7 +74,7 @@ def configure_lapi(settings): save_config(config_path, config) -def configure_lapi_credentials(settings): +def configure_lapi_credentials(settings: dict[str, str]): config_path = '/usr/local/etc/crowdsec/local_api_credentials.yaml' config = load_config(config_path) @@ -73,13 +84,13 @@ def configure_lapi_credentials(settings): save_config(config_path, config) -def configure_bouncer(settings): +def configure_bouncer(settings: dict[str, str]): config_path = '/usr/local/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml' config = load_config(config_path) config['log_dir'] = '/var/log/crowdsec' - config['blacklists_ipv4'] = 'crowdsec_blacklists' - config['blacklists_ipv6'] = 'crowdsec6_blacklists' + config['blacklists_ipv4'] = 'crowdsec_blocklists' + config['blacklists_ipv6'] = 'crowdsec6_blocklists' config['retry_initial_connect'] = True config['pf'] = {'anchor_name': ''} @@ -89,10 +100,35 @@ def configure_bouncer(settings): save_config(config_path, config) +def enroll(settings: dict[str, str]): + enroll_key = settings.get('enroll_key') + if enroll_key: + try: + p = subprocess.run(['cscli', 'capi', 'status'], check=True, text=True, stdout=subprocess.PIPE) + if "instance is enrolled" in p.stdout: + logging.info("crowdsec instance is already enrolled") + return + except subprocess.CalledProcessError: + return + except Exception as e: + logging.error("could not run command 'cscli' to perform enrollment: %s", e) + + try: + logging.info("enrolling crowdsec instance, please accept the enrollment on https://app.crowdsec.net") + _ = subprocess.run( + ['cscli', 'console', 'enroll', '-e', 'context', enroll_key], + check=True, text=True) + except subprocess.CalledProcessError as e: + logging.error("enrollment failed: %s", e) + return + except Exception as e: + logging.error("could not run command 'cscli' to perform enrollment: %s", e) + + def main(): try: with open('/usr/local/etc/crowdsec/opnsense/settings.json') as f: - settings = json.load(f) + settings = cast(dict[str, str], json.load(f)) except FileNotFoundError: logging.info("settings.json not found, won't change crowdsec config") return @@ -100,6 +136,7 @@ def main(): configure_agent(settings) configure_lapi(settings) configure_lapi_credentials(settings) + enroll(settings) configure_bouncer(settings) diff --git a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh index ce4660c50..9ecdd3f8e 100755 --- a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh +++ b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh @@ -2,7 +2,7 @@ # This script is run # - when the plugin is installed (by +POST_INSTALL.post) -# - when saving the "settings" form (which calls /api/crowdsec/service/reload) +# - when saving the "settings" form (which calls /api/crowdsec/service/reconfigure) # - by hand, running "configctl crowdsec reconfigure" set -e diff --git a/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf b/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf index 371cf485d..020fc4669 100644 --- a/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf +++ b/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf @@ -57,10 +57,35 @@ parameters:--id %s type:script_output message:crowdsec decisions delete -[hub-items] -command:/usr/local/bin/cscli hub list -o json +[collections-list] +command:/usr/local/bin/cscli collections list -o json type:script_output -message:crowdsec hub list +message:crowdsec collections list + +[scenarios-list] +command:/usr/local/bin/cscli scenarios list -o json +type:script_output +message:crowdsec scenarios list + +[parsers-list] +command:/usr/local/bin/cscli parsers list -o json +type:script_output +message:crowdsec parsers list + +[postoverflows-list] +command:/usr/local/bin/cscli postoverflows list -o json +type:script_output +message:crowdsec postoverflows list + +[appsec-rules-list] +command:/usr/local/bin/cscli appsec-rules list -o json +type:script_output +message:crowdsec appsec-rules list + +[appsec-configs-list] +command:/usr/local/bin/cscli appsec-configs list -o json +type:script_output +message:crowdsec appsec-configs list [machines-list] command:/usr/local/bin/cscli machines list -o json diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d index 6c0f4e941..de9a90fad 100644 --- a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d @@ -1,5 +1,9 @@ # DO NOT EDIT THIS FILE -- OPNsense auto-generated file -{% if helpers.exists('OPNsense.crowdsec.general.agent_enabled') and OPNsense.crowdsec.general.agent_enabled|default("1") == "1" %} +{% if + (helpers.exists('OPNsense.crowdsec.general.agent_enabled') and OPNsense.crowdsec.general.agent_enabled|default("1") == "1") + or + (helpers.exists('OPNsense.crowdsec.general.lapi_enabled') and OPNsense.crowdsec.general.lapi_enabled|default("1") == "1") +%} crowdsec_enable="YES" {% else %} crowdsec_enable="NO" diff --git a/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec-misc.js b/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec-misc.js new file mode 100644 index 000000000..23468edbd --- /dev/null +++ b/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec-misc.js @@ -0,0 +1,47 @@ +/* global moment, $ */ +/* exported CrowdSec */ +/* eslint no-undef: "error" */ +/* eslint semi: "error" */ + +const CrowdSec = (function () { + 'use strict'; + + function _humanizeDate(text) { + return moment(text).fromNow(); + } + + const formatters = { + yesno: function(column, row) { + const val = row[column.id]; + if (val) { + return ''; + } else { + return ''; + } + }, + + datetime: function (column, row) { + const val = row[column.id]; + const parsed = moment(val); + if (!val) { + return ''; + } + if (!parsed.isValid()) { + console.error('Cannot parse timestamp: %s', val); + return '???'; + } + return $('
        ') + .attr({ + 'data-toggle': 'tooltip', + 'data-placement': 'left', + title: parsed.format(), + }) + .text(_humanizeDate(val)) + .prop('outerHTML'); + }, + }; + + return { + formatters: formatters, + }; +})(); diff --git a/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec.js b/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec.js deleted file mode 100644 index 09f16c12e..000000000 --- a/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec.js +++ /dev/null @@ -1,429 +0,0 @@ -/* global moment, $ */ -/* exported CrowdSec */ -/* eslint no-undef: "error" */ -/* eslint semi: "error" */ - -var CrowdSec = (function () { - 'use strict'; - - var crowdsec_path = '/usr/local/etc/crowdsec/'; - var _refreshTemplate = ''; - - var _dataFormatters = { - yesno: function (column, row) { - return _yesno2html(row[column.id]); - }, - - delete: function (column, row) { - var val = row.id; - if (isNaN(val)) { - return ''; - } - return ''; - }, - - duration: function (column, row) { - var duration = row[column.id]; - if (!duration) { - return 'n/a'; - } - return $('
        ').attr({ - 'data-toggle': 'tooltip', - 'data-placement': 'left', - title: duration - }).text(_humanizeDuration(duration)).prop('outerHTML'); - }, - - datetime: function (column, row) { - var dt = row[column.id]; - var parsed = moment(dt); - if (!dt) { - return ''; - } - if (!parsed.isValid()) { - console.error('Cannot parse timestamp: %s', dt); - return '???'; - } - return $('
        ').attr({ - 'data-toggle': 'tooltip', - 'data-placement': 'left', - title: parsed.format() - }).text(_humanizeDate(dt)).prop('outerHTML'); - } - }; - - function _parseDuration (duration) { - var re = /(-?)(?:(?:(\d+)h)?(\d+)m)?(\d+).\d+(m?)s/m; - var matches = duration.match(re); - var seconds = 0; - - if (!matches.length) { - throw new Error('Unable to parse the following duration: ' + duration + '.'); - } - if (typeof matches[2] !== 'undefined') { - seconds += parseInt(matches[2], 10) * 3600; // hours - } - if (typeof matches[3] !== 'undefined') { - seconds += parseInt(matches[3], 10) * 60; // minutes - } - if (typeof matches[4] !== 'undefined') { - seconds += parseInt(matches[4], 10); // seconds - } - if (parseInt(matches[5], 10) === 'm') { - // units in milliseconds - seconds *= 0.001; - } - if (parseInt(matches[1], 10) === '-') { - // negative - seconds = -seconds; - } - return seconds; - } - - function _updateFreshness (selector, timestamp) { - var $freshness = $(selector).find('.actionBar .freshness'); - if (timestamp) { - $freshness.data('refresh_timestamp', timestamp); - } else { - timestamp = $freshness.data('refresh_timestamp'); - } - var howlongHuman = '???'; - var howlongms; - if (timestamp) { - howlongms = moment() - moment(timestamp); - howlongHuman = moment.duration(howlongms).humanize(); - } - $freshness.text(howlongHuman + ' ago'); - } - - function _addFreshness (selector) { - // this creates one timer per tab - var freshnessTemplate = 'Last refresh: '; - $(selector).find('.actionBar').prepend(freshnessTemplate); - setInterval(function () { - _updateFreshness(selector); - }, 5000); - } - - function _humanizeDate (text) { - return moment(text).fromNow(); - } - - function _humanizeDuration (text) { - return moment.duration(_parseDuration(text), 'seconds').humanize(); - } - - function _yesno2html (val) { - if (val) { - return ''; - } else { - return ''; - } - } - - function _decisionsByType (decisions) { - var dectypes = {}; - if (!decisions) { - return ''; - } - decisions.map(function (decision) { - // TODO ignore negative expiration? - dectypes[decision.type] = dectypes[decision.type] ? (dectypes[decision.type] + 1) : 1; - }); - var ret = ''; - var type; - for (type in dectypes) { - if (ret !== '') { - ret += ' '; - } - ret += (type + ':' + dectypes[type]); - } - return ret; - } - - function _initService () { - $.ajax({ - url: '/api/crowdsec/service/status', - cache: false - }).done(function (data) { - // TODO handle errors - var crowdsecStatus = data['crowdsec-status']; - if (crowdsecStatus === 'unknown') { - crowdsecStatus = 'Unknown'; - } else { - crowdsecStatus = _yesno2html(crowdsecStatus === 'running'); - } - $('#crowdsec-status').html(crowdsecStatus); - - var crowdsecFirewallStatus = data['crowdsec-firewall-status']; - if (crowdsecFirewallStatus === 'unknown') { - crowdsecFirewallStatus = 'Unknown'; - } else { - crowdsecFirewallStatus = _yesno2html(crowdsecFirewallStatus === 'running'); - } - $('#crowdsec-firewall-status').html(crowdsecFirewallStatus); - }); - } - - function _initDebug () { - $.ajax({ - url: '/api/crowdsec/service/debug', - cache: false - }).done(function (data) { - $('#debug pre').text(data.message); - }); - } - - function _initTab (selector, url, dataCallback) { - var $tab = $(selector); - if ($tab.find('table.bootgrid-table').length) { - return; - } - $tab.find('table'). - on('initialized.rs.jquery.bootgrid', function () { - $(_refreshTemplate).on('click', function () { - _refreshTab(selector, url, dataCallback); - }).insertBefore($tab.find('.actionBar .actions .dropdown:first')); - _addFreshness(selector); - _refreshTab(selector, url, dataCallback); - }). - bootgrid({ - caseSensitive: false, - formatters: _dataFormatters - }); - } - - function _refreshTab (selector, url, dataCallback) { - $.ajax({ - url: url, - cache: false - }).done(dataCallback); - _updateFreshness(selector, moment()); - } - - function _initMachines () { - var url = '/api/crowdsec/machines/get'; - var dataCallback = function (data) { - var rows = []; - data.map(function (row) { - rows.push({ - name: row.machineId, - ip_address: row.ipAddress || ' ', - last_update: row.updated_at || ' ', - validated: row.isValidated, - version: row.version || ' ' - }); - }); - $('#machines table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab('#machines', url, dataCallback); - } - - function _initCollections () { - var url = '/api/crowdsec/hub/get'; - var dataCallback = function (data) { - var rows = []; - data.collections.map(function (row) { - rows.push({ - name: row.name, - status: row.status, - local_version: row.local_version || ' ', - local_path: row.local_path ? row.local_path.replace(crowdsec_path, '') : ' ', - description: row.description || ' ' - }); - }); - $('#collections table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab('#collections', url, dataCallback); - } - - function _initScenarios () { - var url = '/api/crowdsec/hub/get'; - var dataCallback = function (data) { - var rows = []; - data.scenarios.map(function (row) { - rows.push({ - name: row.name, - status: row.status, - local_version: row.local_version || ' ', - local_path: row.local_path ? row.local_path.replace(crowdsec_path, '') : ' ', - description: row.description || ' ' - }); - }); - $('#scenarios table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab('#scenarios', url, dataCallback); - } - - function _initParsers () { - var url = '/api/crowdsec/hub/get'; - var dataCallback = function (data) { - var rows = []; - data.parsers.map(function (row) { - rows.push({ - name: row.name, - status: row.status, - local_version: row.local_version || ' ', - local_path: row.local_path ? row.local_path.replace(crowdsec_path, '') : ' ', - description: row.description || ' ' - }); - }); - $('#parsers table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab('#parsers ', url, dataCallback); - } - - function _initPostoverflows () { - var url = '/api/crowdsec/hub/get'; - var dataCallback = function (data) { - var rows = []; - data.postoverflows.map(function (row) { - rows.push({ - name: row.name, - status: row.status, - local_version: row.local_version || ' ', - local_path: row.local_path ? row.local_path.replace(crowdsec_path, '') : ' ', - description: row.description || ' ' - }); - }); - $('#postoverflows table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab('#postoverflows ', url, dataCallback); - } - - function _initBouncers () { - var url = '/api/crowdsec/bouncers/get'; - var dataCallback = function (data) { - var rows = []; - data.map(function (row) { - // TODO - remove || ' ' later, it was fixed for 1.3.3 - rows.push({ - name: row.name, - ip_address: row.ip_address || ' ', - valid: row.revoked ? false : true, - last_pull: row.last_pull, - type: row.type || ' ', - version: row.version || ' ' - }); - }); - $('#bouncers table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab('#bouncers ', url, dataCallback); - } - - function _initAlerts () { - var url = '/api/crowdsec/alerts/get'; - var dataCallback = function (data) { - var rows = []; - data.map(function (row) { - rows.push({ - id: row.id, - value: row.source.scope + (row.source.value ? (':' + row.source.value) : ''), - reason: row.scenario || ' ', - country: row.source.cn || ' ', - as: row.source.as_name || ' ', - decisions: _decisionsByType(row.decisions) || ' ', - created_at: row.created_at - }); - }); - $('#alerts table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab('#alerts ', url, dataCallback); - } - - function _initDecisions () { - var url = '/api/crowdsec/decisions/get'; - var dataCallback = function (data) { - var rows = []; - data.map(function (row) { - row.decisions.map(function (decision) { - // ignore deleted decisions - if (decision.duration.startsWith('-')) { - return; - } - rows.push({ - // search will break on empty values when using .append(). so we use spaces - delete: '', - id: decision.id, - source: decision.origin || ' ', - scope_value: decision.scope + (decision.value ? (':' + decision.value) : ''), - reason: decision.scenario || ' ', - action: decision.type || ' ', - country: row.source.cn || ' ', - as: row.source.as_name || ' ', - events_count: row.events_count, - // XXX pre-parse duration to seconds, and integer type, for sorting - expiration: decision.duration || ' ', - alert_id: row.id || ' ' - }); - }); - }); - $('#decisions table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab('#decisions ', url, dataCallback); - } - - function deleteDecision (decisionId) { - var $modal = $('#delete-decision-modal'); - $modal.find('.modal-title').text('Delete decision #' + decisionId); - $modal.find('.modal-body').text('Are you sure?'); - $modal.find('#delete-decision-confirm').on('click', function () { - $.ajax({ - // XXX handle errors - url: '/api/crowdsec/decisions/delete/' + decisionId, - type: 'DELETE', - success: function (result) { - if (result && result.message === 'OK') { - $('#decisions table').bootgrid('remove', [decisionId]); - $modal.modal('hide'); - } - } - }); - }); - $modal.modal('show'); - } - - function init () { - _initService(); - - $('#machines_tab').on('click', _initMachines); - $('#collections_tab').on('click', _initCollections); - $('#scenarios_tab').on('click', _initScenarios); - $('#parsers_tab').on('click', _initParsers); - $('#postoverflows_tab').on('click', _initPostoverflows); - $('#bouncers_tab').on('click', _initBouncers); - $('#alerts_tab').on('click', _initAlerts); - $('#decisions_tab').on('click', _initDecisions); - - $('[data-toggle="tooltip"]').tooltip(); - - if (window.location.hash) { - // activate a tab from the hash, if it exists - $(window.location.hash + '_tab').click(); - } else { - // otherwise, machines - $('#machines_tab').click(); - } - - $(window).on('hashchange', function (e) { - $(window.location.hash + '_tab').click(); - }); - - if (new URLSearchParams(window.location.search).has('debug')) { - $('#debug_tab').show().on('click', _initDebug); - } - - // navigation - if (window.location.hash !== '') { - $('a[href="' + window.location.hash + '"]').click(); - } - $('.nav-tabs a').on('shown.bs.tab', function (e) { - history.pushState(null, null, e.target.hash); - }); - } - - return { - deleteDecision: deleteDecision, - init: init - }; -}()); diff --git a/security/etpro-telemetry/Makefile b/security/etpro-telemetry/Makefile index c45747de6..d206a5d1c 100644 --- a/security/etpro-telemetry/Makefile +++ b/security/etpro-telemetry/Makefile @@ -1,8 +1,8 @@ PLUGIN_NAME= etpro-telemetry -PLUGIN_VERSION= 1.7 -PLUGIN_REVISION= 5 +PLUGIN_VERSION= 1.8 PLUGIN_COMMENT= ET Pro Telemetry Edition PLUGIN_MAINTAINER= ad@opnsense.org +PLUGIN_DEPENDS= py${PLUGIN_PYTHON}-netaddr PLUGIN_WWW= https://docs.opnsense.org/manual/etpro_telemetry.html PLUGIN_TIER= 2 diff --git a/security/etpro-telemetry/src/opnsense/www/js/widgets/ETProTelemetry.js b/security/etpro-telemetry/src/opnsense/www/js/widgets/ETProTelemetry.js index 8ec7b63b2..046736bc7 100644 --- a/security/etpro-telemetry/src/opnsense/www/js/widgets/ETProTelemetry.js +++ b/security/etpro-telemetry/src/opnsense/www/js/widgets/ETProTelemetry.js @@ -41,7 +41,7 @@ export default class ETProTelemetry extends BaseTableWidget { async onWidgetTick() { const data = await this.ajaxCall('/api/diagnostics/proofpoint_et/status'); - if (data['sensor_status'].toLowerCase() == 'active') { + if (data['sensor_status'].length) { $('#etpro_sensor_status').text(data['sensor_status']); $('#etpro_event_received').text(data['event_received']); $('#etpro_last_rule_download').text(data['last_rule_download']); diff --git a/security/intrusion-detection-content-pt-open/LICENSE b/security/intrusion-detection-content-pt-open/LICENSE new file mode 100644 index 000000000..227437845 --- /dev/null +++ b/security/intrusion-detection-content-pt-open/LICENSE @@ -0,0 +1,24 @@ +(C) 2024 JSC Positive Technologies. All rights reserved. + +Definitions + +“Program” refers to any copyrightable work (including rule sets for open source network threat detection engine Suricata) and associated documentation files licensed under this License, accessible at: https://rules.ptsecurity.com “License” means the terms of this license agreement which apply to the Program. +“Licensee” refers to individuals or legal entities accessing and/or using the Program. +“Modify” a work (part of the work) means to make any change, including translation of the Program from one language into another, except for adaptation. +“Copyright holder” means JSС Positive Technologies as the holder of the exclusive right to the Program. + +Legal Usage + +The Licensee is hereby granted free of charge the rights to use, copy, publish, distribute, sublicense, and/or sell copies of the Program for non-commercial and commercial use subject to the following conditions: +· The above copyright notice shall be included in all copies or substantial portions of the Program. +· Neither the name of the Copyright holder nor the names of its contributors may be used to endorse or promote programs in which the Program was integrated without specific prior written permission. +· Redistributions of the Program must retain the above copyright notice and the full text of the License. +No permission is hereby granted to the Licensee to modify the Program and distribute the modified Program. However, for the avoidance of doubt, the Licensee is granted the right to integrate the original Program into other programs and distribute such programs. + +Applicable law + +This License is governed by the laws of the Russian Federation. The rules of the article 1286.1 of the Civil Code of the Russian Federation are applicable to this License. + +Disclaimer + +THIS PROGRAM IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS”. UNDER NO CIRCUMSTANCES THE COPYRIGHT HOLDER IS LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES RESULTING FROM (I) THE LICENSEE'S USE OF THE PROGRAM; (II) THE LICENSEE'S INTERPRETATION AND APPLICATION OF ANY FILES, METHODS, OR ANY OTHER INFORMATION PROVIDED ON OR THROUGH THE PROGRAM; (III) THE FAILURE OF THE PROGRAM TO MEET THE LICENSEE'S EXPECTATIONS. IF, NOTWITHSTANDING THE OTHER PROVISIONS OF THIS LISENCE, THE COPYRIGHT HOLDER IS FORCED TO BEAR RESPONSIBILITY TO THE LICENSEE FOR ANY LOSSES RELATED TO THE LICENSEE'S USE OF THE PROGRAM, THE COPYRIGHT HOLDER’S LIABILITY SHALL IN NO CASE EXCEED THE EQUIVALENT OF 10 (TEN) U.S. DOLLARS. diff --git a/security/intrusion-detection-content-pt-open/Makefile b/security/intrusion-detection-content-pt-open/Makefile new file mode 100644 index 000000000..5190a1fd9 --- /dev/null +++ b/security/intrusion-detection-content-pt-open/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= intrusion-detection-content-ptopen +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= IDS Positive Technologies ESC ruleset +PLUGIN_MAINTAINER= kulikov.a@gmail.com +PLUGIN_WWW= https://rules.ptsecurity.com + +.include "../../Mk/plugins.mk" diff --git a/security/intrusion-detection-content-pt-open/pkg-descr b/security/intrusion-detection-content-pt-open/pkg-descr new file mode 100644 index 000000000..ff3dc8920 --- /dev/null +++ b/security/intrusion-detection-content-pt-open/pkg-descr @@ -0,0 +1,13 @@ +IDS PT ESC open ruleset designed to detect a variety of network threats, +including those communicated under TLS. + +PT Rules is an open-source project focused on enhancing network security +through proactive threat detection. As the PT Expert Security Center attack +detection team, we are a dedicated group of cybersecurity experts committed +to improve network security through open-source initiatives. + +Don't forget to define the $DC_SERVERS rule-variable if you want to use the +protection rules against DCShadow/DCSync attacks. + +LICENSE: https://rules.ptsecurity.com/view/LICENSE.txt +WWW: https://rules.ptsecurity.com/ diff --git a/security/intrusion-detection-content-pt-open/src/opnsense/scripts/suricata/metadata/rules/pt-open.xml b/security/intrusion-detection-content-pt-open/src/opnsense/scripts/suricata/metadata/rules/pt-open.xml new file mode 100644 index 000000000..632c0bdf1 --- /dev/null +++ b/security/intrusion-detection-content-pt-open/src/opnsense/scripts/suricata/metadata/rules/pt-open.xml @@ -0,0 +1,11 @@ + + + + + ptopen-attacks.rules + ptopen-info.rules + ptopen-malware.rules + ptopen-tools.rules + ptopen-windows.rules + + diff --git a/security/maltrail/pkg-descr b/security/maltrail/pkg-descr index 69ad13f61..cbc444bf1 100644 --- a/security/maltrail/pkg-descr +++ b/security/maltrail/pkg-descr @@ -35,7 +35,7 @@ Changelog 1.5 -* Change whitelisting format (by @jkellerer) +* Change whitelisting format (contributed by jkellerer) * Add alienvault to disabled feeds 1.4 diff --git a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/General.xml b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/General.xml index 4d21fc9b7..d789be99b 100644 --- a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/General.xml +++ b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/General.xml @@ -4,23 +4,23 @@ 0.0.2 - 1 + 1 Y - 0 + 0 Y - 86400 + 86400 Y - 9ab3cd9d67bf49d01f6a2e33d0bd9bc804ddbe6ce1ff5d219c42624851db5dbc + 9ab3cd9d67bf49d01f6a2e33d0bd9bc804ddbe6ce1ff5d219c42624851db5dbc Y - Y + Y N diff --git a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Sensor.xml b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Sensor.xml index 4a97c5757..54512111f 100644 --- a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Sensor.xml +++ b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Sensor.xml @@ -4,11 +4,11 @@ 0.0.3 - 0 + 0 Y - 0 + 0 Y @@ -21,14 +21,14 @@ N - 8337 + 8337 Y N - 514 + 514 Y diff --git a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Server.xml b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Server.xml index 94df269c2..a9db94285 100644 --- a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Server.xml +++ b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Server.xml @@ -4,20 +4,20 @@ 0.0.2 - 0 + 0 Y - 0 + 0 Y - 0.0.0.0 + 0.0.0.0 Y Please provide a valid hostname or IP address. - 8338 + 8338 Y diff --git a/security/netbird/Makefile b/security/netbird/Makefile new file mode 100644 index 000000000..94edc0d4f --- /dev/null +++ b/security/netbird/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= netbird +PLUGIN_VERSION= 1.1 +PLUGIN_DEPENDS= netbird +PLUGIN_COMMENT= Peer-to-peer VPN that seamlessly connects your devices +PLUGIN_MAINTAINER= dev@netbird.io +PLUGIN_WWW= https://netbird.io + +.include "../../Mk/plugins.mk" diff --git a/security/netbird/pkg-descr b/security/netbird/pkg-descr new file mode 100644 index 000000000..e3c155b98 --- /dev/null +++ b/security/netbird/pkg-descr @@ -0,0 +1,18 @@ +NetBird is an open-source WireGuard-based overlay network combined with +Zero Trust Network Access, providing secure and reliable connectivity +to internal resources. + +Key features: +- Zero-config VPN: Easily create secure connections between devices without +manual network setup. +- Built on WireGuard: Leverages WireGuard's high-performance encryption for +fast and secure communication. +- Self-hosted or Cloud-managed: Users can deploy their own NetBird management +server or use NetBird Cloud for centralized control. +- Access Control & Routing: Fine-grained access control policies and automatic +network routing simplify connectivity. +- This FreeBSD port provides the NetBird client daemon and CLI tools, allowing +FreeBSD systems to join a NetBird mesh network and securely communicate with +other peers. + +For more details, visit: https://netbird.io diff --git a/security/netbird/src/etc/inc/plugins.inc.d/netbird.inc b/security/netbird/src/etc/inc/plugins.inc.d/netbird.inc new file mode 100644 index 000000000..1d6b7fce0 --- /dev/null +++ b/security/netbird/src/etc/inc/plugins.inc.d/netbird.inc @@ -0,0 +1,70 @@ +general->enable->isEmpty(); +} + +function netbird_services() +{ + $services = []; + if (!netbird_enabled()) { + return $services; + } + + $services[] = [ + 'description' => gettext('NetBird'), + 'configd' => [ + 'restart' => ['netbird restart'], + 'start' => ['netbird start'], + 'stop' => ['netbird stop'], + ], + 'name' => 'netbird', + 'pidfile' => '/var/run/netbird.pid', + ]; + + return $services; +} + +function netbird_configure() +{ + return [ + 'netbird_sync_config' => ['netbird_configure_do'] + ]; +} + +function netbird_configure_do($verbose = false) +{ + service_log('Sync NetBird config...', $verbose); + (new \OPNsense\Netbird\Settings())->syncConfig(); + service_log("done.\n", $verbose); +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/AuthenticationController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/AuthenticationController.php new file mode 100644 index 000000000..4691c3668 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/AuthenticationController.php @@ -0,0 +1,102 @@ +managementUrl->__toString(); + $setupKey = $mdl->setupKey->__toString(); + + $defaultKey = '00000000-0000-0000-0000-000000000000'; + if (!empty($setupKey) && $setupKey !== $defaultKey) { + $visiblePart = substr($setupKey, 0, 4); + $maskedKey = $visiblePart . str_repeat('*', max(4, strlen($setupKey) - 4)); + } else { + $maskedKey = $defaultKey; + } + + return [ + 'authentication' => [ + 'managementUrl' => $managementUrl, + 'setupKey' => $maskedKey + ] + ]; + } + + public function upAction() + { + $backend = new Backend(); + $mdl = new Authentication(); + + $status = json_decode($backend->configdRun("netbird status-json"), true); + $connected = $status['management']['connected'] ?? false; + + if (json_last_error() === JSON_ERROR_NONE && $connected === true) { + $backend->configdRun("netbird down"); + } + + $managementUrl = $mdl->managementUrl->__toString(); + $setupKey = $mdl->setupKey->__toString(); + + $result = $backend->configdpRun("netbird up-setup-key", array($managementUrl, $setupKey)); + return ['result' => trim($result)]; + } + + public function downAction(): array + { + $backend = new Backend(); + + $status = json_decode($backend->configdRun("netbird status-json"), true); + $connected = $status['management']['connected'] ?? false; + + if (json_last_error() === JSON_ERROR_NONE && $connected === true) { + $result = $backend->configdRun("netbird down"); + return ['result' => trim($result)]; + } + return ['result' => 'already disconnected or not running']; + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/ServiceController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/ServiceController.php new file mode 100644 index 000000000..df7000d12 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/ServiceController.php @@ -0,0 +1,43 @@ +configdRun("netbird sync-config"); + if (stripos($result, 'done') === false) { + return [ + 'result' => 'failed to sync config: ' . $result, + ]; + } + + $status = json_decode($backend->configdRun("netbird status-json"), true); + $connected = $status['management']['connected'] ?? false; + if (json_last_error() === JSON_ERROR_NONE && $connected === true) { + $backend->configdRun("netbird down"); + $backend->configdRun("netbird up"); + } + + return ['result' => 'synced']; + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/StatusController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/StatusController.php new file mode 100644 index 000000000..7a91116e8 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/StatusController.php @@ -0,0 +1,55 @@ +configdRun("netbird status-json"), true); + if (json_last_error() === JSON_ERROR_NONE && is_array($status)) { + return $status; + } + return []; + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/AuthenticationController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/AuthenticationController.php new file mode 100644 index 000000000..bc8790f2a --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/AuthenticationController.php @@ -0,0 +1,42 @@ +view->authenticationForm = $this->getForm("authentication"); + $this->view->pick('OPNsense/Netbird/authentication'); + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/SettingsController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/SettingsController.php new file mode 100644 index 000000000..b3677b198 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/SettingsController.php @@ -0,0 +1,42 @@ +view->settingsForm = $this->getForm("settings"); + $this->view->pick('OPNsense/Netbird/settings'); + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/StatusController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/StatusController.php new file mode 100644 index 000000000..55797b515 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/StatusController.php @@ -0,0 +1,41 @@ +view->pick('OPNsense/Netbird/status'); + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/authentication.xml b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/authentication.xml new file mode 100644 index 000000000..75111fdaf --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/authentication.xml @@ -0,0 +1,14 @@ +
        + + authentication.managementUrl + + text + Base URL of the management service + + + authentication.setupKey + + text + Set the authentication setup key + +
        diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/settings.xml b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/settings.xml new file mode 100644 index 000000000..2f5f4224f --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/settings.xml @@ -0,0 +1,103 @@ +
        + + header + + + + settings.general.enable + + checkbox + Enable NetBird + + + settings.general.wireguardPort + + text + Wireguard interface listening port + + + header + + + + settings.firewall.allowConfig + + checkbox + Allow the client to filter traffic with its built-in firewall. If disabled, you must assign the NetBird interface and manage firewall rules via OPNsense firewall management. + Additionally, NetBird routing and DNS may not function as intended. + + + settings.firewall.blockInboundConnection + + checkbox + Block all inbound connections to the local machine from the WireGuard interface and any routed networks + + + header + + + + settings.ssh.enable + + checkbox + Allows incoming SSH connections + + + header + + + + settings.dns.enable + + checkbox + Allows the client to resolve and configure DNS on the host + + + header + + + + settings.routing.accessLan + + checkbox + Allow access to local networks (LAN) when using this peer as a routing peer or exit-node + + + settings.routing.acceptClientRoutes + + checkbox + Accept and process client routes received from the management + + + settings.routing.acceptServerRoutes + + checkbox + Enable this peer to act as a router for server routes received from the management + + + header + + + + settings.postquantum.enableRosenpass + + checkbox + Enable the Rosenpass to provide post-quantum secure connections (Experimental) + + + settings.postquantum.rosenpassPermissive + + checkbox + Enable Rosenpass permissive mode + + + header + + + + settings.syslog.logLevel + + dropdown + Set the syslog logging level. Setting a certain log level will cause all messages of the specified and more severe log levels to be logged. + +
        diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/ACL/ACL.xml b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/ACL/ACL.xml new file mode 100644 index 000000000..bb0b3272f --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + VPN: NetBird + + ui/netbird/* + api/netbird/* + + + diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Authentication.php b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Authentication.php new file mode 100644 index 000000000..3dc00c2a2 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Authentication.php @@ -0,0 +1,35 @@ + + //OPNsense/netbird/authentication + NetBird authentication + 1.0.0 + + + Y + https://api.netbird.io:443 + + + + /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i + Please specify a valid setup key. + + + diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Menu/Menu.xml b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Menu/Menu.xml new file mode 100644 index 000000000..642302153 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Menu/Menu.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.php b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.php new file mode 100644 index 000000000..0de5371db --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.php @@ -0,0 +1,64 @@ +general->wireguardPort->__toString(); + $config["ServerSSHAllowed"] = $this->ssh->enable->__toString() == 1; + $config["DisableFirewall"] = $this->firewall->allowConfig->__toString() != 1; + $config["BlockInbound"] = $this->firewall->blockInboundConnection->__toString() == 1; + $config["DisableDNS"] = $this->dns->enable->__toString() != 1; + $config["BlockLANAccess"] = $this->routing->accessLan->__toString() != 1; + $config["DisableClientRoutes"] = $this->routing->acceptClientRoutes->__toString() != 1; + $config["DisableServerRoutes"] = $this->routing->acceptServerRoutes->__toString() != 1; + $config["RosenpassEnabled"] = $this->postquantum->enableRosenpass->__toString() == 1; + $config["RosenpassPermissive"] = $this->postquantum->rosenpassPermissive->__toString() == 1; + + + $result = file_put_contents($target, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + if ($result === false) { + syslog(LOG_ERR, "netbird: failed to write updated configuration to $target"); + } + } +} diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.xml b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.xml new file mode 100644 index 000000000..a99ce9985 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.xml @@ -0,0 +1,80 @@ + + //OPNsense/netbird/settings + NetBird settings + 1.1.0 + + + + 0 + Y + + + 51820 + Y + 1 + 65535 + Please specify a valid port. + + + + + 1 + Y + + + 0 + Y + + + + + 0 + Y + + + + + 1 + Y + + + + + 1 + Y + + + 1 + Y + + + 1 + Y + + + + + 0 + Y + + + 0 + Y + + + + + Y + info + + fatal + error + warn + info + debug + trace + + + + + diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Status.php b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Status.php new file mode 100644 index 000000000..da3298cf1 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Status.php @@ -0,0 +1,35 @@ + + :memory: + NetBird status + + + diff --git a/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/authentication.volt b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/authentication.volt new file mode 100644 index 000000000..7d2652c4d --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/authentication.volt @@ -0,0 +1,111 @@ +{# + # Copyright (C) 2025 Ralph Moser, PJ Monitoring GmbH + # Copyright (C) 2025 squared GmbH + # Copyright (C) 2025 Christopher Linn, BackendMedia IT-Services GmbH + # Copyright (C) 2025 NetBird GmbH + # 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. + #} + + + + +
        + {{ partial("layout_partials/base_form",['fields':authenticationForm,'id':'frmAuthentication']) }} +
        + diff --git a/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/settings.volt b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/settings.volt new file mode 100644 index 000000000..7c505a7a1 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/settings.volt @@ -0,0 +1,56 @@ +{# + # Copyright (C) 2025 Ralph Moser, PJ Monitoring GmbH + # Copyright (C) 2025 squared GmbH + # Copyright (C) 2025 Christopher Linn, BackendMedia IT-Services GmbH + # Copyright (C) 2025 NetBird GmbH + # 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. + #} + + +
        + {{ partial("layout_partials/base_form",['fields':settingsForm,'id':'frmSettings']) }} +
        +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/netbird/service/reconfigure', 'data_service_widget': 'netbird'}) }} diff --git a/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/status.volt b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/status.volt new file mode 100644 index 000000000..bea4167a4 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/status.volt @@ -0,0 +1,281 @@ +{# + # Copyright (C) 2025 Ralph Moser, PJ Monitoring GmbH + # Copyright (C) 2025 squared GmbH + # Copyright (C) 2025 Christopher Linn, BackendMedia IT-Services GmbH + # Copyright (C) 2025 NetBird GmbH + # 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. + #} + + +
        +
        +
        +

        {{ lang._('Connection Status') }}

        +
        +
        + +
        +
        +
        +
        +

        {{ lang._('Package Versions') }}

        +
        +
        +
        +
        diff --git a/security/netbird/src/opnsense/service/conf/actions.d/actions_netbird.conf b/security/netbird/src/opnsense/service/conf/actions.d/actions_netbird.conf new file mode 100644 index 000000000..78ae61961 --- /dev/null +++ b/security/netbird/src/opnsense/service/conf/actions.d/actions_netbird.conf @@ -0,0 +1,50 @@ +[start] +command:/usr/local/etc/rc.d/netbird start +parameters: +type:script +message:starting netbird + +[stop] +command:/usr/local/etc/rc.d/netbird stop +parameters: +type:script +message:stopping netbird + +[restart] +command:/usr/local/etc/rc.d/netbird restart +parameters: +type:script +message:restarting netbird + +[status] +command:/usr/local/etc/rc.d/netbird status +errors:no +type:script_output +message:get netbird status + +[up] +command:/usr/local/bin/netbird up +type:script +message:set netbird up + +[down] +command:/usr/local/bin/netbird down +type:script +message:set netbird down + +[status-json] +command:/usr/local/bin/netbird status --json +errors:no +type:script_output +message:get netbird status in json format + +[up-setup-key] +command:/usr/local/bin/netbird up +parameters: -m %s -k %s +type:script_output +message:set netbird up with setup key + +[sync-config] +command:/usr/local/sbin/pluginctl -c netbird_sync_config +type:script_output +message:sync netbird configuration diff --git a/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/+TARGETS b/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/+TARGETS new file mode 100644 index 000000000..ee852218f --- /dev/null +++ b/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/+TARGETS @@ -0,0 +1 @@ +netbird:/etc/rc.conf.d/netbird diff --git a/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/netbird b/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/netbird new file mode 100644 index 000000000..e6191178e --- /dev/null +++ b/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/netbird @@ -0,0 +1,8 @@ +{% if OPNsense.netbird.settings.general.enable == '1' %} +netbird_enable="YES" +netbird_setup="/usr/local/sbin/pluginctl -c netbird_sync_config" +netbird_logfile="syslog" +netbird_loglevel="{{ OPNsense.netbird.settings.syslog.logLevel }}" +{% else %} +netbird_enable="NO" +{% endif %} diff --git a/security/netbird/src/opnsense/service/templates/OPNsense/Syslog/local/netbird.conf b/security/netbird/src/opnsense/service/templates/OPNsense/Syslog/local/netbird.conf new file mode 100644 index 000000000..c16a43df7 --- /dev/null +++ b/security/netbird/src/opnsense/service/templates/OPNsense/Syslog/local/netbird.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [netbird]. +################################################################### +filter f_local_netbird { + program("netbird"); +}; diff --git a/security/openvpn-legacy/Makefile b/security/openvpn-legacy/Makefile new file mode 100644 index 000000000..62af37531 --- /dev/null +++ b/security/openvpn-legacy/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= openvpn-legacy +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= OpenVPN legacy support +PLUGIN_DEPENDS= # openvpn +PLUGIN_MAINTAINER= ad@opnsense.org +PLUGIN_TIER= 2 + +.include "../../Mk/plugins.mk" diff --git a/security/openvpn-legacy/pkg-descr b/security/openvpn-legacy/pkg-descr new file mode 100644 index 000000000..aba0dbd28 --- /dev/null +++ b/security/openvpn-legacy/pkg-descr @@ -0,0 +1 @@ +This plugin adds the legacy OpenVPN server/client configuration pages. diff --git a/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/ACL/ACL.xml b/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/ACL/ACL.xml new file mode 100644 index 000000000..006053043 --- /dev/null +++ b/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/ACL/ACL.xml @@ -0,0 +1,14 @@ + + + VPN: OpenVPN: Client + + vpn_openvpn_client.php* + + + + VPN: OpenVPN: Server + + vpn_openvpn_server.php* + + + diff --git a/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Menu/Menu.xml b/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Menu/Menu.xml new file mode 100644 index 000000000..c611e9fc6 --- /dev/null +++ b/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Menu/Menu.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/security/openvpn-legacy/src/www/vpn_openvpn_client.php b/security/openvpn-legacy/src/www/vpn_openvpn_client.php new file mode 100644 index 000000000..bded6628b --- /dev/null +++ b/security/openvpn-legacy/src/www/vpn_openvpn_client.php @@ -0,0 +1,1231 @@ + + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/openvpn.inc"); + +$a_client = &config_read_array('openvpn', 'openvpn-client'); + +$vpnid = 0; +$act = null; +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + if (isset($_GET['dup']) && isset($a_client[$_GET['dup']])) { + $configId = $_GET['dup']; + } elseif (isset($_GET['id']) && isset($a_client[$_GET['id']])) { + $id = $_GET['id']; + $configId = $id; + } + + if (isset($_GET['act'])) { + $act = $_GET['act']; + } + + $pconfig = array(); + // set defaults + $pconfig['autokey_enable'] = "yes"; // just in case the modes switch + $pconfig['autotls_enable'] = "yes"; // just in case the modes switch + $pconfig['tlsmode'] = "auth"; + $pconfig['digest'] = "SHA1"; + $pconfig['verbosity_level'] = 1; // Default verbosity is 1 + + // edit existing. + if (isset($configId)) { + // 1 on 1 copy of config attributes + $copy_fields = "auth_user,auth_pass,disable,mode,protocol,interface + ,local_port,server_addr,server_port,resolve_retry,remote_random,reneg-sec + ,proxy_addr,proxy_port,proxy_user,proxy_passwd,proxy_authtype,description + ,custom_options,ns_cert_type,dev_mode,tlsmode,caref,certref,crypto,digest + ,tunnel_network,tunnel_networkv6,remote_network,remote_networkv6,use_shaper + ,compression,passtos,route_no_pull,route_no_exec,verbosity_level"; + + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (isset($a_client[$configId][$fieldname])) { + $pconfig[$fieldname] = $a_client[$configId][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + + // load / convert + if (!empty($a_client[$configId]['ipaddr'])) { + $pconfig['interface'] = $pconfig['interface'] . '|' . $a_client[$configId]['ipaddr']; + } + + if (isset($a_client[$configId]['tls'])) { + $pconfig['tls'] = base64_decode($a_client[$configId]['tls']); + } else { + $pconfig['tls'] = null; + $pconfig['tlsmode'] = null; + } + + if (isset($a_client[$configId]['shared_key'])) { + $pconfig['shared_key'] = base64_decode($a_client[$configId]['shared_key']); + } else { + $pconfig['shared_key'] = null ; + } + + if (isset($id)) { + $vpnid = $a_client[$id]['vpnid']; + } + } elseif ($act=="new") { + // create new + $pconfig['interface'] = 'any'; + $init_fields = "auth_user,auth_pass,disable,mode,protocol,interface + ,local_port,server_addr,server_port,resolve_retry,remote_random,reneg-sec + ,proxy_addr,proxy_port,proxy_user,proxy_passwd,proxy_authtype,description + ,custom_options,ns_cert_type,dev_mode,caref,certref,crypto,digest,tlsmode + ,tunnel_network,tunnel_networkv6,remote_network,remote_networkv6,use_shaper + ,compression,passtos,route_no_pull,route_no_exec,verbosity_level"; + + foreach (explode(",", $init_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + $pconfig = $_POST; + $input_errors = array(); + if (isset($_POST['id']) && isset($a_client[$_POST['id']])) { + $id = $_POST['id']; + } + if (isset($_POST['act'])) { + $act = $_POST['act']; + } + + if ($act == "del") { + $response = ["status" => "failed", "message" => gettext("not found")]; + if (isset($id) && !empty($a_client[$id])) { + openvpn_delete('client', $a_client[$id]); + unset($a_client[$id]); + write_config(); + $response = ["status" => "ok"]; + } + echo json_encode($response); + exit; + } elseif ($act == "del_x") { + if (!empty($pconfig['rule']) && is_array($pconfig['rule'])) { + foreach ($pconfig['rule'] as $rulei) { + $vpn_id = !empty($a_client[$rulei]) ? $a_client[$rulei]['vpnid'] : null; + if (!empty($a_client[$rulei])) { + openvpn_delete('client', $a_client[$rulei]); + unset($a_client[$rulei]); + } + } + write_config(); + } + header(url_safe('Location: /vpn_openvpn_client.php')); + exit; + } elseif ($act == "move"){ + // move selected items + if (!isset($id)) { + // if id not set/found, move to end + $id = count($a_client); + } + $a_client = legacy_move_config_list_items($a_client, $id, $pconfig['rule']); + write_config(); + header(url_safe('Location: /vpn_openvpn_client.php')); + exit; + } elseif ($act == "toggle") { + if (isset($id)) { + if (isset($a_client[$id]['disable'])) { + unset($a_client[$id]['disable']); + } else { + $a_client[$id]['disable'] = true; + } + write_config(); + openvpn_configure_single($a_client[$id]['vpnid']); + } + header(url_safe('Location: /vpn_openvpn_client.php')); + exit; + } else { + // update client (after validation) + if (isset($id)) { + $vpnid = $a_client[$id]['vpnid']; + } + if (isset($pconfig['mode']) && $pconfig['mode'] != "p2p_shared_key") { + $tls_mode = true; + } else { + $tls_mode = false; + } + + // generate new key + if (!empty($pconfig['autokey_enable'])) { + $pconfig['shared_key'] = openvpn_create_key(); + } + + /* input validation */ + if (strpos($pconfig['interface'], '|') !== false) { + list($iv_iface, $iv_ip) = explode("|", $pconfig['interface']); + } else { + $iv_iface = $pconfig['interface']; + $iv_ip = null; + } + + if (is_ipaddrv4($iv_ip) && (stristr($pconfig['protocol'], "6") !== false)) { + $input_errors[] = gettext("Protocol and IP address families do not match. You cannot select an IPv6 protocol and an IPv4 address."); + } elseif (is_ipaddrv6($iv_ip) && (stristr($pconfig['protocol'], "6") === false)) { + $input_errors[] = gettext("Protocol and IP address families do not match. You cannot select an IPv4 protocol and an IPv6 address."); + } elseif ((stristr($pconfig['protocol'], "6") === false) && !get_interface_ip($iv_iface) && ($pconfig['interface'] != "any")) { + $input_errors[] = gettext("An IPv4 protocol was selected, but the selected interface has no IPv4 address."); + } elseif ((stristr($pconfig['protocol'], "6") !== false) && !get_interface_ipv6($iv_iface) && ($pconfig['interface'] != "any")) { + $input_errors[] = gettext("An IPv6 protocol was selected, but the selected interface has no IPv6 address."); + } + if (!empty($pconfig['local_port'])) { + if (!is_numeric($pconfig['local_port']) || $pconfig['local_port'] < 0 || ($pconfig['local_port'] > 65535)) { + $input_errors[] = gettext("The field 'Local port' must contain a valid port, ranging from 0 to 65535."); + } + $portused = openvpn_port_used($pconfig['protocol'], $pconfig['interface'], $pconfig['local_port'], $vpnid); + if (($portused != $vpnid) && ($portused != 0)) { + $input_errors[] = gettext("The specified 'Local port' is in use. Please select another value"); + } + } + + $server_addr_a = array(); + $server_port_a = array(); + + foreach (array_keys($pconfig['server_addr']) as $i) { + if (empty($pconfig['server_addr'][$i]) && empty($pconfig['server_port'][$i])) { + continue; + } + if (empty($pconfig['server_addr'][$i]) || (!is_domain($pconfig['server_addr'][$i]) && !is_ipaddr($pconfig['server_addr'][$i]))) { + $input_errors[] = gettext("The field 'Server host or address' must contain a valid IP address or domain name.") ; + } + if (empty($pconfig['server_port'][$i]) || !is_numeric($pconfig['server_port'][$i]) || $pconfig['server_port'][$i] < 0 || $pconfig['server_port'][$i] > 65535) { + $input_errors[] = gettext("The field 'Server port' must contain a valid port, ranging from 0 to 65535."); + } + $server_addr_a[] = $pconfig['server_addr'][$i]; + $server_port_a[] = $pconfig['server_port'][$i]; + } + + $pconfig['server_addr'] = implode(',', $server_addr_a); + $pconfig['server_port'] = implode(',', $server_port_a); + + if (empty($pconfig['server_addr']) || empty($pconfig['server_port'])) { + $input_errors[] = gettext("At least one remote server must be specified."); + } + + if (isset($pconfig['reneg-sec']) && $pconfig['reneg-sec'] != "" && (string)((int)$pconfig['reneg-sec']) != $pconfig['reneg-sec']) { + $input_errors[] = gettext("Renegotiate time should contain a valid number of seconds."); + } + + if (!empty($pconfig['proxy_addr'])) { + if (empty($pconfig['proxy_addr']) || (!is_domain($pconfig['proxy_addr']) && !is_ipaddr($pconfig['proxy_addr']))) { + $input_errors[] = gettext("The field 'Proxy host or address' must contain a valid IP address or domain name."); + } + if (empty($pconfig['proxy_port']) || !is_numeric($pconfig['proxy_port']) || $pconfig['proxy_port'] < 0 || ($pconfig['proxy_port'] > 65535)) { + $input_errors[] = gettext("The field 'Proxy port' must contain a valid port, ranging from 0 to 65535."); + } + if (isset($pconfig['proxy_authtype']) && $pconfig['proxy_authtype'] != "none") { + if (empty($pconfig['proxy_user']) || empty($pconfig['proxy_passwd'])) { + $input_errors[] = gettext("User name and password are required for proxy with authentication."); + } + } + } + if ($result = openvpn_validate_cidr($pconfig['tunnel_network'], gettext('IPv4 Tunnel Network'), false, 'ipv4')) { + $input_errors[] = $result; + } + if ($result = openvpn_validate_cidr($pconfig['tunnel_networkv6'], gettext('IPv6 Tunnel Network'), false, 'ipv6')) { + $input_errors[] = $result; + } + if ($result = openvpn_validate_cidr($pconfig['remote_network'], gettext('IPv4 Remote Network'), true, 'ipv4')) { + $input_errors[] = $result; + } + if ($result = openvpn_validate_cidr($pconfig['remote_networkv6'], gettext('IPv6 Remote Network'), true, 'ipv6')) { + $input_errors[] = $result; + } + if (!empty($pconfig['use_shaper']) && (!is_numeric($pconfig['use_shaper']) || ($pconfig['use_shaper'] <= 0))) { + $input_errors[] = gettext("The bandwidth limit must be a positive numeric value."); + } + if (!$tls_mode && empty($pconfig['autokey_enable'])) { + if (!strstr($pconfig['shared_key'], "-----BEGIN OpenVPN Static key V1-----") || + !strstr($pconfig['shared_key'], "-----END OpenVPN Static key V1-----")) { + $input_errors[] = gettext("The field 'Shared Key' does not appear to be valid"); + } + } + if ($tls_mode && !empty($pconfig['tlsmode']) && empty($pconfig['autotls_enable'])) { + if (!strstr($pconfig['tls'], "-----BEGIN OpenVPN Static key V1-----") || + !strstr($pconfig['tls'], "-----END OpenVPN Static key V1-----")) { + $input_errors[] = gettext("The field 'TLS Shared Key' does not appear to be valid"); + } + } + + /* If we are not in shared key mode, then we need the CA/Cert. */ + if (isset($pconfig['mode']) && $pconfig['mode'] != "p2p_shared_key") { + $reqdfields = explode(" ", "caref"); + $reqdfieldsn = array(gettext("Certificate Authority")); + } elseif (empty($pconfig['autokey_enable'])) { + /* We only need the shared key filled in if we are in shared key mode and autokey is not selected. */ + $reqdfields = array('shared_key'); + $reqdfieldsn = array(gettext('Shared key')); + } + + do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); + + if (($pconfig['mode'] != "p2p_shared_key") && empty($pconfig['certref']) && empty($pconfig['auth_user']) && empty($pconfig['auth_pass'])) { + $input_errors[] = gettext("If no Client Certificate is selected, a username and password must be entered."); + } + $prev_opt = (isset($id) && !empty($a_client[$id])) ? $a_client[$id]['custom_options'] : ""; + if ($prev_opt != str_replace("\r\n", "\n", $pconfig['custom_options']) && !userIsAdmin($_SESSION['Username'])) { + $input_errors[] = gettext('Advanced options may only be edited by system administrators due to the increased possibility of privilege escalation.'); + } + + if (count($input_errors) == 0) { + // save data + $client = array(); + // 1 on 1 copy of config attributes + $copy_fields = "auth_user,auth_pass,protocol,dev_mode,local_port,reneg-sec + ,server_addr,server_port,resolve_retry,proxy_addr,proxy_port,remote_random + ,proxy_authtype,proxy_user,proxy_passwd,description,mode,crypto,digest + ,tunnel_network,tunnel_networkv6,remote_network,remote_networkv6 + ,use_shaper,compression,passtos,route_no_pull,route_no_exec,tlsmode + ,verbosity_level,interface"; + + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!empty($pconfig[$fieldname]) || $pconfig[$fieldname] == '0') { + $client[$fieldname] = $pconfig[$fieldname]; + } + } + + // attributes containing some kind of logic + if ($vpnid) { + $client['vpnid'] = $vpnid; + } else { + $client['vpnid'] = openvpn_vpnid_next(); + } + if (isset($pconfig['disable']) && $pconfig['disable'] == "yes") { + $client['disable'] = true; + } + + if (strpos($pconfig['interface'], "|") !== false) { + list($client['interface'], $client['ipaddr']) = explode("|", $pconfig['interface']); + } + $client['custom_options'] = str_replace("\r\n", "\n", $pconfig['custom_options']); + + if ($tls_mode) { + $client['caref'] = $pconfig['caref']; + $client['certref'] = $pconfig['certref']; + if (!empty($pconfig['tlsmode'])) { + if (!empty($pconfig['autotls_enable'])) { + $pconfig['tls'] = openvpn_create_key(); + } + $client['tls'] = base64_encode($pconfig['tls']); + } + } else { + $client['shared_key'] = base64_encode($pconfig['shared_key']); + } + + if (isset($id)) { + $a_client[$id] = $client; + } else { + $a_client[] = $client; + } + + write_config(); + + openvpn_configure_single($client['vpnid']); + + header(url_safe('Location: /vpn_openvpn_client.php')); + exit; + } + } +} + +// escape form output before processing +legacy_html_escape_form_data($pconfig); + +include("head.inc"); + +?> + + + +
        +
        +
        + 0) { + print_input_errors($input_errors); + } + if (isset($savemsg)) { + print_info_box($savemsg); + }?> + +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + + +
        + /> + +
        + + +
        + +
        + + +
        + +
        + +
        + + + + + + + + + + $item): ?> + + + + + + + +
        + + + + + + + + + +
        +
        + /> + +
        + /> + + +
        + +
        + +
        + + + +
        + + +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + + + + +
        +
        +
        +
        +
        + +
        +
        + + +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + +
        + + > + . + +
        + +

        .

        +
        +
        + + + +
        + . + +
        + + +
        + + +
        + + /> + . + +
        + + . +
        +
        + + +
        + + +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + + +
        + + +
        + + +
        + + +
        + + +
        + + +
        + /> + +
        + /> + +
        + /> + +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + + + + +
        + + + +
        + + +
        +
        +
        +
        +
        +
        +
        + + + + + +
          + + + + + +
        +
        +
        +
        +
        + +
        +
        + + +
        +
        + + +
        +
        +
        + diff --git a/security/openvpn-legacy/src/www/vpn_openvpn_server.php b/security/openvpn-legacy/src/www/vpn_openvpn_server.php new file mode 100644 index 000000000..4bf340fe4 --- /dev/null +++ b/security/openvpn-legacy/src/www/vpn_openvpn_server.php @@ -0,0 +1,1710 @@ + + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/openvpn.inc"); + +$a_server = &config_read_array('openvpn', 'openvpn-server'); + +$act = null; +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // fetch id if provided + if (isset($_GET['dup']) && isset($a_server[$_GET['dup']])) { + $configId = $_GET['dup']; + } elseif (isset($_GET['id']) && is_numericint($_GET['id'])) { + $id = $_GET['id']; + $configId = $id; + } + if (isset($_GET['act'])) { + $act = $_GET['act']; + } + $pconfig = array(); + // defaults + $vpnid = 0; + $pconfig['verbosity_level'] = 1; + $pconfig['digest'] = "SHA1"; // OpenVPN Defaults to SHA1 if unset + $pconfig['crypto'] = ""; + $pconfig['tlsmode'] = "auth"; + $pconfig['autokey_enable'] = "yes"; + $pconfig['autotls_enable'] = "yes"; + if (isset($configId) && isset($a_server[$configId])) { + if ($a_server[$configId]['mode'] != "p2p_shared_key") { + $pconfig['cert_depth'] = 1; + } + + // 1 on 1 copy of config attributes + $copy_fields = "mode,protocol,authmode,dev_mode,interface,local_port + ,description,custom_options,crypto,tunnel_network + ,tunnel_networkv6,remote_network,remote_networkv6,gwredir,local_network + ,local_networkv6,maxclients,compression,passtos,client2client + ,dynamic_ip,topology_subnet,serverbridge_dhcp + ,serverbridge_interface,serverbridge_dhcp_start,serverbridge_dhcp_end + ,dns_server1,dns_server2,dns_server3,dns_server4,ntp_server1 + ,ntp_server2,netbios_enable,netbios_ntype,netbios_scope,wins_server1 + ,wins_server2,push_register_dns,push_block_outside_dns,dns_domain,dns_domain_search,local_group + ,client_mgmt_port,verbosity_level,tlsmode,caref,crlref,certref + ,cert_depth,strictusercn,digest,disable,duplicate_cn,vpnid,reneg-sec,use-common-name,cso_login_matching"; + + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (isset($a_server[$configId][$fieldname])) { + $pconfig[$fieldname] = $a_server[$configId][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + + // load / convert + if (!empty($a_server[$configId]['ipaddr'])) { + $pconfig['interface'] = $pconfig['interface'] . '|' . $a_server[$configId]['ipaddr']; + } + if (!empty($a_server[$configId]['shared_key'])) { + $pconfig['shared_key'] = base64_decode($a_server[$configId]['shared_key']); + } else { + $pconfig['shared_key'] = null; + } + if (!empty($a_server[$configId]['tls'])) { + $pconfig['tls'] = base64_decode($a_server[$configId]['tls']); + } else { + $pconfig['tls'] = null; + $pconfig['tlsmode'] = null; + } + } elseif ($act == "new") { + $pconfig['dev_mode'] = "tun"; + $pconfig['interface'] = 'any'; + $pconfig['protocol'] = 'UDP'; + $pconfig['local_port'] = openvpn_port_next($pconfig['protocol']); + $pconfig['cert_depth'] = 1; + // init all fields used in the form + $init_fields = "mode,protocol,authmode,dev_mode,interface,local_port + ,description,custom_options,crypto,tunnel_network + ,tunnel_networkv6,remote_network,remote_networkv6,gwredir,local_network + ,local_networkv6,maxclients,compression,passtos,client2client + ,dynamic_ip,topology_subnet,serverbridge_dhcp + ,serverbridge_interface,serverbridge_dhcp_start,serverbridge_dhcp_end + ,dns_server1,dns_server2,dns_server3,dns_server4,ntp_server1 + ,ntp_server2,netbios_enable,netbios_ntype,netbios_scope,wins_server1 + ,wins_server2,push_register_dns,push_block_outside_dns,dns_domain,dns_domain_search + ,client_mgmt_port,verbosity_level,tlsmode,caref,crlref,certref + ,cert_depth,strictusercn,digest,disable,duplicate_cn,vpnid,shared_key,tls,reneg-sec,use-common-name + ,cso_login_matching"; + foreach (explode(",", $init_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } + + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['id']) && isset($a_server[$_POST['id']])) { + $id = $_POST['id']; + } + if (isset($_POST['act'])) { + $act = $_POST['act']; + } + + if ($act == "del") { + $response = ["status" => "failed", "message" => gettext("not found")]; + if (isset($id) && !empty($a_server[$id])) { + openvpn_delete('server', $a_server[$id]); + unset($a_server[$id]); + write_config(); + $response = ["status" => "ok"]; + } + echo json_encode($response); + exit; + } elseif ($act == "toggle") { + if (isset($id)) { + if (isset($a_server[$id]['disable'])) { + unset($a_server[$id]['disable']); + } else { + $a_server[$id]['disable'] = true; + } + write_config(); + openvpn_configure_single($a_server[$id]['vpnid']); + } + header(url_safe('Location: /vpn_openvpn_server.php')); + exit; + } else { + // action add/update + $input_errors = array(); + $pconfig = $_POST; + + $vpnid = (isset($id) && $a_server[$id]) ? $a_server[$id]['vpnid'] : 0; + $tls_mode = ($pconfig['mode'] != "p2p_shared_key"); + + if (!empty($pconfig['autokey_enable'])) { + $pconfig['shared_key'] = openvpn_create_key(); + } + + // all input validators + if (strpos($pconfig['interface'], '|') !== false) { + list($iv_iface, $iv_ip) = explode("|", $pconfig['interface']); + } else { + $iv_iface = $pconfig['interface']; + $iv_ip = null; + } + + if (is_ipaddrv4($iv_ip) && (stristr($pconfig['protocol'], "6") !== false)) { + $input_errors[] = gettext("Protocol and IP address families do not match. You cannot select an IPv6 protocol and an IPv4 IP address."); + } elseif (is_ipaddrv6($iv_ip) && (stristr($pconfig['protocol'], "6") === false)) { + $input_errors[] = gettext("Protocol and IP address families do not match. You cannot select an IPv4 protocol and an IPv6 IP address."); + } elseif ((stristr($pconfig['protocol'], "6") === false) && !get_interface_ip($iv_iface) && ($pconfig['interface'] != "any")) { + $input_errors[] = gettext("An IPv4 protocol was selected, but the selected interface has no IPv4 address."); + } elseif ((stristr($pconfig['protocol'], "6") !== false) && !get_interface_ipv6($iv_iface) && ($pconfig['interface'] != "any")) { + $input_errors[] = gettext("An IPv6 protocol was selected, but the selected interface has no IPv6 address."); + } + + if (empty($pconfig['authmode']) && (($pconfig['mode'] == "server_user") || ($pconfig['mode'] == "server_tls_user"))) { + $input_errors[] = gettext("You must select a Backend for Authentication if the server mode requires User Auth."); + } + + if ($result = openvpn_validate_port($pconfig['local_port'], gettext('Local port'))) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['tunnel_network'], gettext('IPv4 Tunnel Network'), false, 'ipv4')) { + $input_errors[] = $result; + } elseif (!empty($pconfig['tunnel_network']) && (strpos($pconfig['mode'], "p2p_") === false)) { + // Check IPv4 tunnel_network pool size for Remote Access modes + list($ipv4tunnel_base, $ipv4tunnel_prefix) = explode('/',trim($pconfig['tunnel_network'])); + if ($pconfig['dev_mode'] == "tun") { + if ($ipv4tunnel_prefix > 28 && empty($pconfig['topology_subnet'])) { + $input_errors[] = gettext('A prefix longer than 28 cannot be used with a net30 topology.'); + } elseif ($ipv4tunnel_prefix > 29 && !empty($pconfig['topology_subnet'])) { + $input_errors[] = gettext('A prefix longer than 29 cannot be used for tunnel network.'); + } + } elseif ($pconfig['dev_mode'] == "tap" && $ipv4tunnel_prefix > 29) { + $input_errors[] = gettext('A prefix longer than 29 cannot be used for tunnel network.'); + } + } + + if ($result = openvpn_validate_cidr($pconfig['tunnel_networkv6'], gettext('IPv6 Tunnel Network'), false, 'ipv6')) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['remote_network'], gettext('IPv4 Remote Network'), true, 'ipv4')) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['remote_networkv6'], gettext('IPv6 Remote Network'), true, 'ipv6')) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['local_network'], gettext('IPv4 Local Network'), true, 'ipv4')) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['local_networkv6'], gettext('IPv6 Local Network'), true, 'ipv6')) { + $input_errors[] = $result; + } + + if (!empty($pconfig['local_port'])) { + $portused = openvpn_port_used($pconfig['protocol'], $pconfig['interface'], $pconfig['local_port'], $vpnid); + if ($portused) { + $input_errors[] = gettext("The specified 'Local port' is in use. Please select another value"); + } + } + + if (!$tls_mode && empty($pconfig['autokey_enable'])) { + if (!strstr($pconfig['shared_key'], "-----BEGIN OpenVPN Static key V1-----") || + !strstr($pconfig['shared_key'], "-----END OpenVPN Static key V1-----")) { + $input_errors[] = gettext("The field 'Shared Key' does not appear to be valid"); + } + } + + if ($tls_mode && !empty($pconfig['tlsmode']) && empty($pconfig['autotls_enable'])) { + if (!strstr($pconfig['tls'], "-----BEGIN OpenVPN Static key V1-----") || + !strstr($pconfig['tls'], "-----END OpenVPN Static key V1-----")) { + $input_errors[] = gettext("The field 'TLS Shared Key' does not appear to be valid"); + } + } + + if (!empty($pconfig['dns_domain_search'])) { + $tmp_ok_domain = 0; + $tmp_nok_domain = 0; + foreach (explode(",", $pconfig['dns_domain_search'] ?? "") as $domain) { + if (is_domain($domain)) { + $tmp_ok_domain++; + } else { + $tmp_nok_domain++; + } + } + if ($tmp_nok_domain > 0) { + $input_errors[] = gettext("The field 'DNS Domain search list' must contain valid domain names"); + } elseif ($tmp_ok_domain > 10) { + $input_errors[] = gettext("The field 'DNS Domain search list' may contain max 10 entries"); + } + } + + if (!empty($pconfig['dns_server1']) && !is_ipaddr(trim($pconfig['dns_server1']))) { + $input_errors[] = gettext("The field 'DNS Server #1' must contain a valid IP address"); + } + if (!empty($pconfig['dns_server2']) && !is_ipaddr(trim($pconfig['dns_server2']))) { + $input_errors[] = gettext("The field 'DNS Server #2' must contain a valid IP address"); + } + if (!empty($pconfig['dns_server3']) && !is_ipaddr(trim($pconfig['dns_server3']))) { + $input_errors[] = gettext("The field 'DNS Server #3' must contain a valid IP address"); + } + if (!empty($pconfig['dns_server4']) && !is_ipaddr(trim($pconfig['dns_server4']))) { + $input_errors[] = gettext("The field 'DNS Server #4' must contain a valid IP address"); + } + + if (!empty($pconfig['ntp_server1']) && !is_ipaddr(trim($pconfig['ntp_server1']))) { + $input_errors[] = gettext("The field 'NTP Server #1' must contain a valid IP address"); + } + if (!empty($pconfig['ntp_server2']) && !is_ipaddr(trim($pconfig['ntp_server2']))) { + $input_errors[] = gettext("The field 'NTP Server #2' must contain a valid IP address"); + } + + if (!empty($pconfig['wins_server_enable'])) { + if (!empty($pconfig['wins_server1']) && !is_ipaddr(trim($pconfig['wins_server1']))) { + $input_errors[] = gettext("The field 'WINS Server #1' must contain a valid IP address"); + } + if (!empty($pconfig['wins_server2']) && !is_ipaddr(trim($pconfig['wins_server2']))) { + $input_errors[] = gettext("The field 'WINS Server #2' must contain a valid IP address"); + } + } + + if (!empty($pconfig['client_mgmt_port_enable'])) { + if ($result = openvpn_validate_port($pconfig['client_mgmt_port'], gettext('Client management port'))) { + $input_errors[] = $result; + } + } + + if (!empty($pconfig['maxclients']) && !is_numeric($pconfig['maxclients'])) { + $input_errors[] = gettext("The field 'Concurrent connections' must be numeric."); + } + + /* If we are not in shared key mode, then we need the CA/Cert. */ + if (isset($pconfig['mode']) && $pconfig['mode'] != "p2p_shared_key") { + $reqdfields = explode(" ", "caref certref"); + $reqdfieldsn = array(gettext("Certificate Authority"),gettext("Certificate")); + } elseif (empty($pconfig['autokey_enable'])) { + /* We only need the shared key filled in if we are in shared key mode and autokey is not selected. */ + $reqdfields = array('shared_key'); + $reqdfieldsn = array(gettext('Shared key')); + } + + $reqdfields[] = 'local_port'; + $reqdfieldsn[] = gettext('Local port'); + + if ($pconfig['dev_mode'] != "tap") { + $reqdfields[] = 'tunnel_network,tunnel_networkv6'; + $reqdfieldsn[] = gettext('Tunnel Network'); + } else { + if ($pconfig['serverbridge_dhcp'] && ($pconfig['tunnel_network'] || $pconfig['tunnel_networkv6'])) { + $input_errors[] = gettext("Using a tunnel network and server bridge settings together is not allowed."); + } + if (($pconfig['serverbridge_dhcp_start'] && !$pconfig['serverbridge_dhcp_end']) + || (!$pconfig['serverbridge_dhcp_start'] && $pconfig['serverbridge_dhcp_end'])) { + $input_errors[] = gettext("Server Bridge DHCP Start and End must both be empty, or defined."); + } + if (($pconfig['serverbridge_dhcp_start'] && !is_ipaddrv4($pconfig['serverbridge_dhcp_start']))) { + $input_errors[] = gettext("Server Bridge DHCP Start must be an IPv4 address."); + } + if (($pconfig['serverbridge_dhcp_end'] && !is_ipaddrv4($pconfig['serverbridge_dhcp_end']))) { + $input_errors[] = gettext("Server Bridge DHCP End must be an IPv4 address."); + } + if (ip2ulong($pconfig['serverbridge_dhcp_start']) > ip2ulong($pconfig['serverbridge_dhcp_end'])) { + $input_errors[] = gettext("The Server Bridge DHCP range is invalid (start higher than end)."); + } + } + if (isset($pconfig['reneg-sec']) && $pconfig['reneg-sec'] != "" && (string)((int)$pconfig['reneg-sec']) != $pconfig['reneg-sec']) { + $input_errors[] = gettext("Renegotiate time should contain a valid number of seconds."); + } + + if (!empty($pconfig['certref'])) { + foreach ($config['cert'] as $cert) { + if ($cert['refid'] == $pconfig['certref']) { + if (cert_get_purpose($cert['crt'])['id-kp-serverAuth'] == 'No') { + $input_errors[] = gettext( + sprintf('Certificate %s is not intended for server use.', $cert['descr']) + ); + } + } + } + } + + $prev_opt = (isset($id) && !empty($a_server[$id])) ? $a_server[$id]['custom_options'] : ""; + if ($prev_opt != str_replace("\r\n", "\n", $pconfig['custom_options']) && !userIsAdmin($_SESSION['Username'])) { + $input_errors[] = gettext('Advanced options may only be edited by system administrators due to the increased possibility of privilege escalation.'); + } + + do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); + + if (count($input_errors) == 0) { + // validation correct, save data + $server = array(); + + // delete(rename) old interface so a new TUN or TAP interface can be created. + if (isset($id) && $pconfig['dev_mode'] != $a_server[$id]['dev_mode']) { + openvpn_delete('server', $a_server[$id]); + } + // 1 on 1 copy of config attributes + $copy_fields = "mode,protocol,dev_mode,local_port,description,crypto,digest + ,tunnel_network,tunnel_networkv6,remote_network,remote_networkv6 + ,gwredir,local_network,local_networkv6,maxclients,compression + ,passtos,client2client,dynamic_ip,topology_subnet,local_group + ,serverbridge_dhcp,serverbridge_interface,serverbridge_dhcp_start + ,serverbridge_dhcp_end,dns_domain,dns_domain_search,dns_server1,dns_server2,dns_server3 + ,dns_server4,push_register_dns,push_block_outside_dns,ntp_server1,ntp_server2,netbios_enable + ,netbios_ntype,netbios_scope,verbosity_level,wins_server1,tlsmode + ,wins_server2,client_mgmt_port,strictusercn,reneg-sec,use-common-name,cso_login_matching"; + + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!empty($pconfig[$fieldname]) || $pconfig[$fieldname] == '0') { + $server[$fieldname] = $pconfig[$fieldname]; + } + } + + // attributes containing some kind of logic + if ($vpnid != 0) { + $server['vpnid'] = $vpnid; + } else { + $server['vpnid'] = openvpn_vpnid_next(); + } + + if ($pconfig['disable'] == "yes") { + $server['disable'] = true; + } + if (!empty($pconfig['authmode'])) { + $server['authmode'] = implode(",", $pconfig['authmode']); + } + if (strpos($pconfig['interface'], "|") !== false) { + list($server['interface'], $server['ipaddr']) = explode("|", $pconfig['interface']); + } else { + $server['interface'] = $pconfig['interface']; + } + + $server['custom_options'] = str_replace("\r\n", "\n", $pconfig['custom_options']); + + if ($tls_mode) { + if ($pconfig['tlsmode']) { + if (!empty($pconfig['autotls_enable'])) { + $pconfig['tls'] = openvpn_create_key(); + } + $server['tls'] = base64_encode($pconfig['tls']); + } + foreach (['caref', 'crlref', 'certref', 'cert_depth'] as $cpKey) { + if (isset($pconfig[$cpKey])) { + $server[$cpKey] = $pconfig[$cpKey]; + } + } + if (isset($pconfig['mode']) && $pconfig['mode'] == "server_tls_user" && isset($server['strictusercn'])) { + $server['strictusercn'] = $pconfig['strictusercn']; + } + } else { + $server['shared_key'] = base64_encode($pconfig['shared_key']); + } + + if (isset($_POST['duplicate_cn']) && $_POST['duplicate_cn'] == "yes") { + $server['duplicate_cn'] = true; + } + + // update or add to config + if (isset($id) && $a_server[$id]) { + $a_server[$id] = $server; + } else { + $a_server[] = $server; + } + + write_config(); + + openvpn_configure_single($server['vpnid']); + + header(url_safe('Location: /vpn_openvpn_server.php')); + exit; + } elseif (!empty($pconfig['authmode'])) { + $pconfig['authmode'] = implode(",", $pconfig['authmode']); + } + } +} + +include("head.inc"); + +legacy_html_escape_form_data($pconfig); + +?> + + + + +
        +
        +
        + 0) { + print_input_errors($input_errors); + } + if (isset($savemsg)) { + print_info_box($savemsg); + }?> + +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + + +
        + + +
        + /> +
        + +
        + + +
        + +
        + + +
        + +
        + + +
        + +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + +
        + + /> + . + +
        + +

        .

        +
        +
        + + + + +
        . + +
        + + + + +
        . + +
        + + + + +
        . + +
        + +
        + /> + . +
        + +
        + + . +
        +
        + + +
        + + +
        + + + + + +
        + +
        + +
        +
        + /> + +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + + +
        + + +
        + /> + +
        + + +
        + + +
        + +
        + /> + +
        + + +
        + + +
        + + +
        + + +
        + + +
        + + +
        + /> + +
        + /> + +
        + /> + +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + /> + +
        + /> + +
        + /> + +
        + +   + + + +   + + +
        +
        + /> +
        + +
        + +
        + /> + +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + +
        + + + +
        + + +
        + + +
        + /> + +
          + + + + + +
        +
        +
        +
        +
        + + +
        + +
        + + + + + + + + + + + + + + + + + + + + + + +
        + + + +
        + "> + "> + + + / + + + + + + + + + + + + +
        +
        +
        + +
        +
        +
        + + diff --git a/security/q-feeds-connector/+POST_INSTALL.post b/security/q-feeds-connector/+POST_INSTALL.post new file mode 100755 index 000000000..47481ad32 --- /dev/null +++ b/security/q-feeds-connector/+POST_INSTALL.post @@ -0,0 +1,3 @@ +#!/bin/sh + +/usr/local/sbin/pluginctl -s cron restart diff --git a/security/q-feeds-connector/Makefile b/security/q-feeds-connector/Makefile new file mode 100644 index 000000000..4b4a22416 --- /dev/null +++ b/security/q-feeds-connector/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= q-feeds-connector +PLUGIN_VERSION= 1.2 +PLUGIN_TIER= 2 +PLUGIN_COMMENT= Connector for Q-Feeds threat intel +PLUGIN_MAINTAINER= devel@qfeeds.com + +.include "../../Mk/plugins.mk" diff --git a/security/q-feeds-connector/README.md b/security/q-feeds-connector/README.md new file mode 100644 index 000000000..b18fb8cc5 --- /dev/null +++ b/security/q-feeds-connector/README.md @@ -0,0 +1,48 @@ +# plugin to connect to QFeeds threat platform + +Register for a token at : https://qfeeds.com/opnsense/ + +# command line control + +Settings are persisted in `/usr/local/etc/qfeeds.conf`, using the following format: + +``` +[api] +key=tip_xxxxxxxx +``` + +Our commandline tool contains all actions used by the UI, which is practical for debuggging. + +``` +usage: qfeedsctl.py [-h] [--target_dir TARGET_DIR] [-f] [-v] [{fetch_index,fetch,show_index,firewall_load,update,stats} ...] + +positional arguments: + {fetch_index,fetch,show_index,firewall_load,update,stats} + +options: + -h, --help show this help message and exit + --target_dir TARGET_DIR + -f forced (auto index) + -v verbose output +``` + +The index is the driver for most actions, which is a json encoded file in `/var/db/qfeeds-tables/index.json`. + +Actions supported: + +* fetch_index --> download the index file +* fetch --> download the lists +* firewall_load --> collect ip lists into pre-defined firewall tables +* update [sleep when almost time] --> run fetch_index --> fetch --> firewall_load (to be used by cron) +* show_index --> dumps the index +* stats --> dumps feed information +* logs --> dumps firewall log information for aliases offered by Q-Feeds + + +Example usage: + +``` +/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py update +``` + +**Fetch index and update lists when updated remotely** diff --git a/security/q-feeds-connector/pkg-descr b/security/q-feeds-connector/pkg-descr new file mode 100644 index 000000000..796c7e92b --- /dev/null +++ b/security/q-feeds-connector/pkg-descr @@ -0,0 +1,19 @@ +Connector for Q-Feeds threat intel + +Plugin Changelog +================ + +1.2 + +* fix rule parsing +* bug fixes for unbound blocklist support + +1.1 + +* remove QfeedsStatus as the new table defaults are different +* add unbound blocklist support +* Events: fix empty interface names + +1.0 + +* Intial release version diff --git a/security/q-feeds-connector/src/etc/inc/plugins.inc.d/qfeeds.inc b/security/q-feeds-connector/src/etc/inc/plugins.inc.d/qfeeds.inc new file mode 100644 index 000000000..0d70f598a --- /dev/null +++ b/security/q-feeds-connector/src/etc/inc/plugins.inc.d/qfeeds.inc @@ -0,0 +1,35 @@ +configdRun('template reload OPNsense/QFeeds')); + if (strtolower($res) != 'ok') { + throw new UserException(sprintf(gettext("Unable to update settings (%s)"), $res)); + } + $res = trim($backend->configdRun('qfeeds reconfigure')); + if (strpos($res, 'EXIT OK') === false) { + throw new UserException($res); + } + return ['status' => 'ok', 'output' => $res]; + } + + public function searchFeedsAction() + { + $records = []; + $data = json_decode((new Backend())->configdRun('qfeeds info') ?? '[]', true); + if (!empty($data) && !empty($data['feeds'])) { + $records = $data['feeds']; + foreach ($records as &$record) { + $record['licensed'] = $record['licensed'] ? '1' : '0'; + } + } + return $this->searchRecordsetBase($records); + } + + public function searchEventsAction() + { + $records = []; + $ifnames = []; + foreach (Config::getInstance()->object()->interfaces->children() as $key => $node) { + if (!empty((string)$node->if)) { + $ifnames[(string)$node->if] = !empty((string)($node->descr)) ? (string)($node->descr) : strtoupper($key); + } + } + $data = json_decode((new Backend())->configdRun('qfeeds logs') ?? '[]', true); + if (!empty($data) && !empty($data['rows'])) { + foreach ($data['rows'] as $row) { + $records[] = [ + 'timestamp' => $row[0], + 'interface' => $ifnames[$row[1]] ?? $row[1], + 'direction' => $row[2], + 'source' => $row[3], + 'destination' => $row[4], + ]; + } + } + return $this->searchRecordsetBase($records); + } + + public function statsAction() + { + $stats = json_decode((new Backend())->configdRun('qfeeds stats'), true); + if (!empty($stats) && !empty($stats['feeds'])) { + $info = json_decode((new Backend())->configdRun('qfeeds info'), true); + if (!empty($info) && !empty($info['feeds'])) { + $feeds = []; + foreach ($info['feeds'] as $feed) { + $feeds[$feed['feed_type']] = $feed; + } + foreach ($stats['feeds'] as &$feed) { + if (isset($feeds[$feed['name']])) { + $tmp = $feeds[$feed['name']]; + $feed['updated_at'] = $tmp['updated_at']; + $feed['next_update'] = $tmp['next_update']; + $feed['licensed'] = $tmp['licensed']; + } + } + } + } + return $stats; + } +} diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/IndexController.php b/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/IndexController.php new file mode 100644 index 000000000..d2017c9e6 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/IndexController.php @@ -0,0 +1,40 @@ +view->formSettings = $this->getForm("settings"); + $this->view->pick('OPNsense/QFeeds/index'); + } +} diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/forms/settings.xml b/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/forms/settings.xml new file mode 100644 index 000000000..0469ed6ca --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/forms/settings.xml @@ -0,0 +1,18 @@ +
        + + header + + + + connect.general.apikey + + text + click here]]> + + + connect.general.enable_unbound_bl + + checkbox + Use domain feeds in Unbound DNS blocklist, requires blocklists to be enabled in order to have effect + +
        diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/QfeedsAliases.php b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/QfeedsAliases.php new file mode 100644 index 000000000..f6e33c770 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/QfeedsAliases.php @@ -0,0 +1,56 @@ +configdRun('qfeeds index') ?? '', true) ?? []; + if (is_array($payload) && !empty($payload['feeds'])) { + foreach ($payload['feeds'] as $feed) { + if ($feed['type'] == 'ip' && !empty($feed['licensed'])) { + $name = '__qfeeds_' . $feed['feed_type']; + $result[$name] = [ + 'enabled' => '1', + 'counters' => '1', + 'name' => $name, + 'type' => 'external', + 'description' => $feed['feed_type'], + 'content' => '' + ]; + } + } + } + return $result; + } +} diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/ACL/ACL.xml b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/ACL/ACL.xml new file mode 100644 index 000000000..ca1468d63 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + Services: QFeeds + + ui/q_feeds/* + api/q_feeds/* + + + diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Connector.php b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Connector.php new file mode 100644 index 000000000..f81bceb4b --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Connector.php @@ -0,0 +1,39 @@ + + //OPNsense/QFeedsConnector + 1.0.0 + QFeeds connector + + + + + + + diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Menu/Menu.xml b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Menu/Menu.xml new file mode 100644 index 000000000..dda28fa4d --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Menu/Menu.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/views/OPNsense/QFeeds/index.volt b/security/q-feeds-connector/src/opnsense/mvc/app/views/OPNsense/QFeeds/index.volt new file mode 100644 index 000000000..52425088d --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/views/OPNsense/QFeeds/index.volt @@ -0,0 +1,136 @@ +{# + +OPNsense® is Copyright © 2025 by Deciso B.V. +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. + +#} + + + + +
        +
        + {{ partial("layout_partials/base_form",['fields':formSettings,'id':'frm_settings'])}} +
        +
        + + + + + + + + + + + + +
        {{ lang._('Description') }}{{ lang._('Type') }}{{ lang._('Updated at') }}{{ lang._('Next update') }}{{ lang._('Licensed') }}
        +
        +
        + + + + + + + + + + + + + +
        {{ lang._('Timestamp') }}{{ lang._('Interface') }}{{ lang._('Direction') }}{{ lang._('Source') }}{{ lang._('Destination') }}
        +
        {{ lang._('Collected events from the firewall log for QFeed aliases') }}
        +
        +
        + +
        +
        +
        +
        + +

        +
        +
        +
        diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py new file mode 100755 index 000000000..f0f029c1d --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py @@ -0,0 +1,187 @@ +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" +import os +import subprocess +import time +import ujson +from datetime import datetime +from lib.api import Api +from lib.log import PFLogCrawler +from lib.file import LockedFile + + +class QFeedsActions: + def __init__(self, target_dir, forced=False): + self._target_dir = target_dir + self._forced = forced + + @classmethod + def list_actions(cls): + return [ + 'fetch_index', + 'fetch', + 'show_index', + 'firewall_load', + 'unbound_load', + 'update', + 'stats', + 'logs' + ] + + @property + def index_file(self): + return "%s/index.json" % self._target_dir + + @property + def index(self): + if not os.path.exists(self.index_file) and self._forced: + # require index file to get feeds + list(self.fetch_index()) + elif not os.path.exists(self.index_file): + return {} + data = ujson.load(open(self.index_file)) or {} + if type(data) is dict: + for feed in data.get('feeds', []): + feed['local_filename'] = "%s/%s.txt" % (self._target_dir, feed['feed_type']) + feed['updated_at_dt'] = datetime.fromisoformat(feed['updated_at']).timestamp() + feed['next_update_dt'] = datetime.fromisoformat(feed['next_update']).timestamp() + + return data + + def _file_stat(self, filename): + if not os.path.exists(filename): + return 0 + return os.stat(filename).st_mtime + + def fetch_index(self): + if not os.path.isdir(self._target_dir): + os.makedirs(self._target_dir) + with LockedFile(self.index_file) as f: + payload = Api().licenses() + f.truncate() + f.write(ujson.dumps(payload)) + yield 'downloaded index to %s' % f.filename + + def show_index(self): + yield ujson.dumps(self.index) + + def fetch(self): + for feed in self.index.get('feeds', []): + if feed['licensed'] and feed['updated_at_dt'] != self._file_stat(feed['local_filename']): + with LockedFile(feed['local_filename']) as f: + counter = 0 + for entry in Api().fetch(feed['feed_type']): + if counter == 0: + f.truncate() + f.write("%s\n" % entry) + counter += 1 + os.utime(feed['local_filename'], (feed['updated_at_dt'], feed['updated_at_dt'])) + yield "downloaded %d entries into %s [%s]" % (counter, feed['local_filename'], feed['updated_at']) + elif feed['licensed']: + yield "skipped %s [%s]" % (feed['local_filename'], feed['updated_at']) + + def firewall_load(self): + for feed in self.index.get('feeds', []): + if feed['licensed'] and os.path.exists(feed['local_filename']) and feed['type'] == 'ip': + table_name = '__qfeeds_%s' % feed['feed_type'] + sp = subprocess.run( + ['/sbin/pfctl', '-t', table_name, '-T', 'replace', '-f', feed['local_filename']], + capture_output=True, + text=True + ) + yield 'load feed %s [%s]' % (feed['feed_type'], sp.stderr.strip().replace("\n", " ")) + + def unbound_load(self): + bl_conf = '/usr/local/etc/unbound/qfeeds-blocklists.conf' + if os.path.exists(bl_conf) and os.path.getsize(bl_conf) > 20: + # when qfeeds-blocklists.conf is ~empty, skip updates + subprocess.run(['/usr/local/sbin/configctl', 'unbound', 'dnsbl']) + yield 'update unbound blocklist' + + def update(self): + update_sleep = 99999 + try: + index_payload = self.index + except TypeError: + # when the index can't be parsed, assume we have none while updating + index_payload = {} + do_update = len(index_payload.get('feeds', [])) == 0 + for feed in index_payload.get('feeds', []): + update_sleep = min(feed['next_update_dt'] - time.time(), update_sleep) + if feed['licensed'] and update_sleep <= 300: # 5 minute cron interval + do_update = True + if do_update: + if 0 < update_sleep <= 300: + time.sleep(update_sleep) + for action in ['fetch_index', 'fetch', 'firewall_load', 'unbound_load']: + yield from getattr(self, action)() + + def stats(self): + result = {'feeds': []} + for feed in self.index.get('feeds', []): + if feed['licensed'] and os.path.exists(feed['local_filename']) and feed['type'] == 'ip': + table_name = '__qfeeds_%s' % feed['feed_type'] + sp = subprocess.Popen( + ['/sbin/pfctl', '-t', table_name, '-vT', 'show'], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True + ) + record = { + 'name': feed['feed_type'], + 'total_entries': 0, + 'packets_blocked': 0, + 'bytes_blocked': 0, + 'addresses_blocked': 0 + } + while (line := sp.stdout.readline()): + if line.startswith(' '): + record['total_entries'] += 1 + elif 'Packets:' in line and 'Packets: 0 ' not in line: + parts = line.split() + if parts[3].isdigit() and parts[5].isdigit() and parts[0].lower().find('block') > 0: + record['packets_blocked'] += int(parts[3]) + record['bytes_blocked'] += int(parts[5]) + record['addresses_blocked'] += 1 + + result['feeds'].append(record) + result['totals'] = { + 'entries': sum(r['total_entries'] for r in result['feeds']), + # assumes no overlaps in datafeeds + 'addresses_blocked': sum(r['addresses_blocked'] for r in result['feeds']), + 'packets_blocked': sum(r['packets_blocked'] for r in result['feeds']), + 'bytes_blocked': sum(r['bytes_blocked'] for r in result['feeds']), + } + + yield ujson.dumps(result) + + def logs(self): + feeds = [] + for feed in self.index.get('feeds', []): + if feed['type'] == 'ip': + feeds.append('__qfeeds_%s' % feed['feed_type']) + + yield ujson.dumps({'rows': PFLogCrawler(feeds).find()}) diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/api.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/api.py new file mode 100755 index 000000000..6f2bb6a3c --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/api.py @@ -0,0 +1,70 @@ +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" +import os +import requests +from configparser import ConfigParser + + +class QFeedsConfig: + api_key = None + + def __init__(self): + config_filename = '/usr/local/etc/qfeeds.conf' + if os.path.isfile(config_filename): + cnf = ConfigParser() + cnf.read(config_filename) + if cnf.has_section('api') and cnf.has_option('api', 'key'): + self.api_key = cnf.get('api', 'key') + + +class Api: + def __init__(self): + self.api_key = QFeedsConfig().api_key + + def licenses(self): + r = requests.get( + url='https://api.qfeeds.com/licenses.php', + auth=('api_token', self.api_key), + timeout=60, + headers={'User-Agent': 'Q-Feeds_OPNsense'} + ) + r.raise_for_status() + return r.json() + + def fetch(self, feed): + r = requests.get( + url='https://api.qfeeds.com/api.php', + params={'feed_type': feed}, + auth=('api_token', self.api_key), + headers={'User-Agent': 'Q-Feeds_OPNsense'}, + stream=True, + timeout=60 + ) + r.raise_for_status() + for line in r.raw: + entry = line.decode().strip() + if entry: + yield entry diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/file.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/file.py new file mode 100755 index 000000000..89fbf8ae8 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/file.py @@ -0,0 +1,52 @@ +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" +import fcntl + + +class LockedFile: + def __init__(self, filename): + self._filename = filename + self._fh = None + + def __enter__(self): + self._fh = open(self._filename, 'a+') + fcntl.flock(self._fh, fcntl.LOCK_EX | fcntl.LOCK_NB) + return self + + def __exit__(self, ex_type, ex_value, traceback): + if self._fh: + self._fh.close() + + def truncate(self): + self._fh.seek(0) + self._fh.truncate() + + def write(self, data): + self._fh.write(data) + + @property + def filename(self): + return self._filename diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/log.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/log.py new file mode 100755 index 000000000..fd2c506db --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/log.py @@ -0,0 +1,81 @@ +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" + +import glob +import time +import subprocess +import ipaddress + +def is_ip_address(value): + try: + ipaddress.ip_address(value) + return True + except ValueError: + return False + + +class PFLogCrawler: + def __init__(self, table_names:list=[]): + self._table_names = table_names + self._rule_ids = set() + self._collect_rule_ids() + + def _collect_rule_ids(self): + self._rule_ids = set() + sp = subprocess.run(['/sbin/pfctl', '-sr'], capture_output=True, text=True) + for line in sp.stdout.split("\n"): + for table in self._table_names: + if line.find("<%s>" % table) > 0: + self._rule_ids.add(line.split()[-1].strip('"')) + if 'label "' in line: + quote_start = line.find('"', line.find('label "')) + quote_end = line.find('"', quote_start + 1) + if quote_end > quote_start: + self._rule_ids.add(line[quote_start + 1:quote_end]) + + @staticmethod + def _parse_log_line(line): + # quick scan for datetime, interface, direction, source, dest + parts = line.split() + fw_line = parts[-1].split(',') # strip syslog + return [parts[1], fw_line[4], fw_line[7]] + [x for x in fw_line if is_ip_address(x)] + + def find(self, max_time=60, max_results=50000): + result = [] + start_time = time.time() + rows_processed = 0 + for filename in sorted(glob.glob("/var/log/filter/filter_*.log"), reverse=True): + with open(filename) as f_in: + for idx, line in enumerate(f_in): + for rule_id in self._rule_ids: + if rule_id in line: + result.append(self._parse_log_line(line)) + rows_processed +=1 + break # inner loop + if (idx % 100000 == 0 and time.time() - start_time > max_time) or rows_processed >= max_results: + return result + + return result diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/qfeedsctl.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/qfeedsctl.py new file mode 100755 index 000000000..7973d59f4 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/qfeedsctl.py @@ -0,0 +1,63 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" + +import argparse +import sys +import ujson +from requests.exceptions import HTTPError, Timeout +from lib import QFeedsActions + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--target_dir', default='/var/db/qfeeds-tables') + parser.add_argument('-f', help='forced (auto index)' , default=False, action='store_true') + parser.add_argument('-v', help='verbose output' , default=False, action='store_true') + parser.add_argument("action", choices=QFeedsActions.list_actions(), nargs='*') + args = parser.parse_args() + if args.v: + # verbose mode + import http.client as http_client + http_client.HTTPConnection.debuglevel = 1 + try: + actions = QFeedsActions(args.target_dir, args.f) + for action in args.action: + for msg in getattr(actions, action)(): + print(msg) + except HTTPError as exc: + print('exit with HTTPError %d (%s)' % (exc.response.status_code, exc.response.text)) + sys.exit(-1) + except Timeout as exc: + print('timeout reaching api endpoint') + sys.exit(-1) + except IOError as e: + print("output filename locked or missing") + sys.exit(-1) + except ujson.JSONDecodeError: + print("JSON decode error") + sys.exit(-1) diff --git a/security/q-feeds-connector/src/opnsense/scripts/unbound/blocklists/qfeeds_bl.py b/security/q-feeds-connector/src/opnsense/scripts/unbound/blocklists/qfeeds_bl.py new file mode 100755 index 000000000..e10428db0 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/unbound/blocklists/qfeeds_bl.py @@ -0,0 +1,58 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" + +import os +from . import BaseBlocklistHandler + +class DefaultBlocklistHandler(BaseBlocklistHandler): + def __init__(self): + super().__init__('/usr/local/etc/unbound/qfeeds-blocklists.conf') + self.priority = 50 + + def get_config(self): + # do not use, unbound worker settings + return {} + + def get_blocklist(self): + # Only return domains if integration is enabled (filenames are offered) + qfeeds_filenames = [] + if self.cnf and self.cnf.has_section('settings'): + if self.cnf.has_option('settings', 'filenames'): + qfeeds_filenames = self.cnf.get('settings', 'filenames').split(',') + + result = {} + for filename in qfeeds_filenames: + bl_shortcode = "qf_%s" % os.path.splitext(os.path.basename(filename).strip())[0] + if os.path.exists(filename): + with open(filename, 'r') as f_in: + for line in f_in: + result[line.strip()] = {'bl': bl_shortcode, 'wildcard': False} + return result + + def get_passlist_patterns(self): + return [] diff --git a/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf b/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf new file mode 100644 index 000000000..e2fba096e --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf @@ -0,0 +1,40 @@ +[reconfigure] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py fetch_index fetch firewall_load unbound_load && echo 'EXIT OK' +parameters: +type:script_output +message:reconfigure QFeeds +errors:no + +[update] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py update +parameters: +type:script_output +message:update QFeeds +errors:no + +[info] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py show_index +parameters: +type:script_output +message:fetch QFeeds info + +[stats] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py stats +parameters: +type:script_output +cache_ttl: 3600 +message:return Qfeeds local stats + +[logs] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py logs +parameters: +type:script_output +cache_ttl: 300 +message:return Qfeeds log data + +[index] +command:cat /var/db/qfeeds-tables/index.json +parameters: +type:script_output +message:return raw QFeeds index file +cache_ttl: 60 diff --git a/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/+TARGETS b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/+TARGETS new file mode 100644 index 000000000..219dc39f8 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/+TARGETS @@ -0,0 +1,2 @@ +qfeeds.conf:/usr/local/etc/qfeeds.conf +qfeeds-blocklists.conf:/usr/local/etc/unbound/qfeeds-blocklists.conf diff --git a/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds-blocklists.conf b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds-blocklists.conf new file mode 100644 index 000000000..611317775 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds-blocklists.conf @@ -0,0 +1,5 @@ +{% if not helpers.empty('OPNsense.QFeedsConnector.general.apikey') and + not helpers.empty('OPNsense.QFeedsConnector.general.enable_unbound_bl') %} +[settings] +filenames=/var/db/qfeeds-tables/malware_domains.txt +{% endif %} diff --git a/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds.conf b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds.conf new file mode 100644 index 000000000..be8460cb8 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds.conf @@ -0,0 +1,4 @@ +{% if not helpers.empty('OPNsense.QFeedsConnector.general.apikey') %} +[api] +key={{OPNsense.QFeedsConnector.general.apikey}} +{% endif %} diff --git a/security/q-feeds-connector/src/opnsense/www/img/QFeeds.png b/security/q-feeds-connector/src/opnsense/www/img/QFeeds.png new file mode 100644 index 000000000..5dd5e38a5 Binary files /dev/null and b/security/q-feeds-connector/src/opnsense/www/img/QFeeds.png differ diff --git a/security/q-feeds-connector/src/opnsense/www/js/widgets/Metadata/QFeeds.xml b/security/q-feeds-connector/src/opnsense/www/js/widgets/Metadata/QFeeds.xml new file mode 100644 index 000000000..faec76e2a --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/www/js/widgets/Metadata/QFeeds.xml @@ -0,0 +1,20 @@ + + + QFeeds.js + + /api/q_feeds/settings/* + + + Q-Feeds Threat Protection + Unable to contact information feed. + Installed Feeds + Database + Size + Blocked + Updated + Next + Licensed + Unlicensed + + + diff --git a/security/q-feeds-connector/src/opnsense/www/js/widgets/QFeeds.js b/security/q-feeds-connector/src/opnsense/www/js/widgets/QFeeds.js new file mode 100644 index 000000000..bf5acbcb1 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/www/js/widgets/QFeeds.js @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2025 Deciso B.V. + * 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. + */ + +export default class QFeeds extends BaseTableWidget { + constructor() { + super(); + } + + getMarkup() { + let $container = $('
        '); + let $sysinfotable = this.createTable('qfeeds-table', { + headerPosition: 'left', + }); + $container.append($sysinfotable); + return $container; + } + + async onWidgetTick() { + return; + } + + async onMarkupRendered() { + let header = $("div.widget.widget-qfeeds").find('.widget-header'); + let title = $('#qfeeds-title'); + let divider = $("div.widget.widget-qfeeds").find('.panel-divider'); + header.css({ + 'background-image': 'URL("/ui/img/QFeeds.png")', + 'background-size': 'auto 50px', + 'background-position': 'center left', + 'margin-top': '0px', + 'mix-blend-mode': 'difference', + 'background-repeat': 'no-repeat' + }); + title.empty(); + title.css({ + 'height': '70px' + }) + divider.hide(); + $("#qfeeds-table").css({ + 'margin-top': '0px', + 'margin-bottom': '5px', + }); + + const data = await this.ajaxCall(`/api/q_feeds/settings/${'stats'}`); + if (!data.feeds.length) { + $('#qfeeds-table').html(`${this.translations.no_feed}`); + return; + } + let rows = []; + let feeds = []; + for (let feed of data.feeds) { + feeds.push( + ` ${feed.name}`, + `
          ${this.translations.last_update}: ${feed.updated_at}
        `, + `
          ${this.translations.next_update}: ${feed.next_update}
        ` + ); + if (feed.licensed) { + feeds.push(`
          ${this.translations.licensed}
        `); + } else { + feeds.push(`
          ${this.translations.unlicensed}
        `); + } + } + rows.push([[this.translations.installed_feeds], feeds]); + let db = [ + `
        ${this.translations.size}: ${data.totals.entries.toLocaleString()}
        `, + `
        ${this.translations.blocked}: ${data.totals.addresses_blocked.toLocaleString()}
        ` + + ]; + rows.push([[this.translations.database], db]); + + super.updateTable('qfeeds-table', rows); + } +} diff --git a/security/softether/Makefile b/security/softether/Makefile deleted file mode 100644 index e7cc86d2e..000000000 --- a/security/softether/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -PLUGIN_NAME= softether -PLUGIN_VERSION= 0.3 -PLUGIN_COMMENT= Cross-platform Multi-protocol VPN Program -PLUGIN_DEPENDS= softether -PLUGIN_MAINTAINER= m.muenz@gmail.com -PLUGIN_DEVEL= yes - -.include "../../Mk/plugins.mk" diff --git a/security/softether/pkg-descr b/security/softether/pkg-descr deleted file mode 100644 index e49e2da52..000000000 --- a/security/softether/pkg-descr +++ /dev/null @@ -1,8 +0,0 @@ -SoftEther VPN ("SoftEther" means "Software Ethernet") is one of -the world's most powerful and easy-to-use multi-protocol VPN -software. It runs on Windows, Linux, Mac, FreeBSD and Solaris. - -SoftEther VPN is open source. You can use SoftEther for any -personal or commercial use for free charge. - -WWW: https://www.softether.org/ diff --git a/security/softether/src/etc/rc.syshook.d/carp/50-softether b/security/softether/src/etc/rc.syshook.d/carp/50-softether deleted file mode 100755 index 71d3e58ae..000000000 --- a/security/softether/src/etc/rc.syshook.d/carp/50-softether +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/local/bin/php - - * Copyright (C) 2004 Scott Ullrich - * 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. - */ - -require_once('config.inc'); -require_once('util.inc'); -require_once('interfaces.inc'); -require_once('plugins.inc.d/softether.inc'); - -if (softether_carp_enabled()) { - // XXX: carp enable/disable mode - $subsystem = !empty($argv[1]) ? $argv[1] : ''; - $type = !empty($argv[2]) ? $argv[2] : ''; - - if ($type != 'MASTER' && $type != 'BACKUP') { - log_msg("Carp '$type' event unknown from source '{$subsystem}'"); - exit(1); - } - - if (!strstr($subsystem, '@')) { - log_msg("Carp '$type' event triggered from wrong source '{$subsystem}'"); - exit(1); - } - - list ($vhid, $iface) = explode('@', $subsystem); - $friendly = convert_real_interface_to_friendly_interface_name($iface); - - if (!(strpos(softether_carp_interfaces(),$friendly) !== false)) { - exit(0); - } - - switch ($type) { - case 'MASTER': - touch('/var/run/softether/CARP_MASTER'); - shell_exec('/usr/local/etc/rc.d/softether_server start'); - break; - case 'BACKUP': - if (file_exists('/var/run/softether/CARP_MASTER')) { - unlink('/var/run/softether/CARP_MASTER'); - } - shell_exec('/usr/local/etc/rc.d/softether_server stop'); - break; - } -} diff --git a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/forms/general.xml b/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/forms/general.xml deleted file mode 100644 index 8e738f5d4..000000000 --- a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/forms/general.xml +++ /dev/null @@ -1,21 +0,0 @@ -
        - - general.enabled - - checkbox - This will activate SoftEther vpnserver process. - - - general.enablecarp - - checkbox - This will activate the vpnserver service only on the master device. - - - general.carpinterfaces - - select_multiple - - Type or select interface. - -
        diff --git a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/ACL/ACL.xml b/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/ACL/ACL.xml deleted file mode 100644 index d7a964e55..000000000 --- a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/ACL/ACL.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - VPN: SoftEther - - ui/softether/* - api/softether/* - - - diff --git a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/General.xml b/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/General.xml deleted file mode 100644 index 05968a34c..000000000 --- a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/General.xml +++ /dev/null @@ -1,24 +0,0 @@ - - //OPNsense/softether/general - Softether configuration - 0.0.1 - - - 0 - Y - - - 0 - Y - - - N - Y - - Y - - /^(?!0).*$/ - - - - diff --git a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/Menu/Menu.xml b/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/Menu/Menu.xml deleted file mode 100644 index 6bebfff37..000000000 --- a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/Menu/Menu.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/security/softether/src/opnsense/mvc/app/views/OPNsense/Softether/general.volt b/security/softether/src/opnsense/mvc/app/views/OPNsense/Softether/general.volt deleted file mode 100644 index 7ddc14d6b..000000000 --- a/security/softether/src/opnsense/mvc/app/views/OPNsense/Softether/general.volt +++ /dev/null @@ -1,68 +0,0 @@ -{# - -OPNsense® is Copyright © 2014 – 2018 by Deciso B.V. -This file is Copyright © 2018 by Michael Muenz -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. - -#} - - - - -
        -
        -
        - {{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_general_settings'])}} -
        -
        - -
        -
        -
        -
        - - diff --git a/security/softether/src/opnsense/scripts/OPNsense/Softether/setup.sh b/security/softether/src/opnsense/scripts/OPNsense/Softether/setup.sh deleted file mode 100755 index f83ff509c..000000000 --- a/security/softether/src/opnsense/scripts/OPNsense/Softether/setup.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -mkdir -p /var/db/softether -mkdir -p /var/log/softether diff --git a/security/softether/src/opnsense/service/conf/actions.d/actions_softether.conf b/security/softether/src/opnsense/service/conf/actions.d/actions_softether.conf deleted file mode 100644 index 6e840fd06..000000000 --- a/security/softether/src/opnsense/service/conf/actions.d/actions_softether.conf +++ /dev/null @@ -1,23 +0,0 @@ -[start] -command:/usr/local/etc/rc.d/softether_server start -parameters: -type:script -message:starting softether - -[stop] -command:/usr/local/etc/rc.d/softether_server stop -parameters: -type:script -message:stopping softether - -[restart] -command:/usr/local/etc/rc.d/softether_server restart -parameters: -type:script -message:restarting softether - -[status] -command:/usr/local/etc/rc.d/softether_server status; exit 0 -parameters: -type:script_output -message:softether status diff --git a/security/softether/src/opnsense/service/templates/OPNsense/Softether/+TARGETS b/security/softether/src/opnsense/service/templates/OPNsense/Softether/+TARGETS deleted file mode 100644 index 04e32a977..000000000 --- a/security/softether/src/opnsense/service/templates/OPNsense/Softether/+TARGETS +++ /dev/null @@ -1 +0,0 @@ -softether_server:/etc/rc.conf.d/softether_server diff --git a/security/softether/src/opnsense/service/templates/OPNsense/Softether/softether_server b/security/softether/src/opnsense/service/templates/OPNsense/Softether/softether_server deleted file mode 100644 index be34375d5..000000000 --- a/security/softether/src/opnsense/service/templates/OPNsense/Softether/softether_server +++ /dev/null @@ -1,9 +0,0 @@ -{% if helpers.exists('OPNsense.softether.general.enabled') and OPNsense.softether.general.enabled == '1' %} -softether_server_setup="/usr/local/opnsense/scripts/OPNsense/Softether/setup.sh" -softether_server_enable="YES" -{% if helpers.exists('OPNsense.softether.general.enablecarp') and OPNsense.softether.general.enablecarp == '1' %} -required_files="/var/run/softether/CARP_MASTER" -{% endif %} -{% else %} -softether_server_enable="NO" -{% endif %} diff --git a/security/strongswan-legacy/Makefile b/security/strongswan-legacy/Makefile new file mode 100644 index 000000000..2f2a02c73 --- /dev/null +++ b/security/strongswan-legacy/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= strongswan-legacy +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= IPsec legacy support +PLUGIN_DEPENDS= # strongswan +PLUGIN_MAINTAINER= ad@opnsense.org +PLUGIN_TIER= 2 + +.include "../../Mk/plugins.mk" diff --git a/security/strongswan-legacy/pkg-descr b/security/strongswan-legacy/pkg-descr new file mode 100644 index 000000000..4d829ceb3 --- /dev/null +++ b/security/strongswan-legacy/pkg-descr @@ -0,0 +1 @@ +This plugin adds the legacy IPsec tunnel and mobile configuration pages. diff --git a/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/ACL/ACL.xml b/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/ACL/ACL.xml new file mode 100644 index 000000000..7441c7c6c --- /dev/null +++ b/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/ACL/ACL.xml @@ -0,0 +1,28 @@ + + + VPN: IPsec: Tunnels [legacy] + + ui/ipsec/tunnels + api/ipsec/tunnel/* + api/ipsec/legacy_subsystem/* + + + + VPN: IPsec: Edit Phase 1 + + vpn_ipsec_phase1.php* + + + + VPN: IPsec: Edit Phase 2 + + vpn_ipsec_phase2.php* + + + + VPN: IPsec: Mobile [legacy] + + vpn_ipsec_mobile.php* + + + diff --git a/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/Menu/Menu.xml b/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/Menu/Menu.xml new file mode 100644 index 000000000..6a33b6cf6 --- /dev/null +++ b/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/Menu/Menu.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/security/strongswan-legacy/src/www/vpn_ipsec_mobile.php b/security/strongswan-legacy/src/www/vpn_ipsec_mobile.php new file mode 100644 index 000000000..23d00589a --- /dev/null +++ b/security/strongswan-legacy/src/www/vpn_ipsec_mobile.php @@ -0,0 +1,310 @@ + + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); +require_once("filter.inc"); +require_once("system.inc"); +require_once("plugins.inc.d/ipsec.inc"); + +config_read_array('ipsec', 'client'); +config_read_array('ipsec', 'phase1'); + +// define formfields +$form_fields = "pool_address,pool_netbits,pool_address_v6,pool_netbits_v6"; + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // pass savemessage + if (isset($_GET['savemsg'])) { + $savemsg = htmlspecialchars($_GET['savemsg']); + } + $pconfig = array(); + // defaults + $pconfig['pool_netbits'] = 24; + $pconfig['pool_netbits_v6'] = 64; + + // copy / initialize $pconfig attributes + foreach (explode(",", $form_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (isset($config['ipsec']['client'][$fieldname])) { + $pconfig[$fieldname] = $config['ipsec']['client'][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + if (isset($config['ipsec']['client']['enable'])) { + $pconfig['enable'] = true; + } + +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + $input_errors = array(); + $pconfig = $_POST; + if (isset($_POST['create'])) { + // create new phase1 entry + header(url_safe('Location: /vpn_ipsec_phase1.php?mobile=true')); + exit; + } elseif (isset($_POST['apply'])) { + // apply changes + ipsec_configure_do(); + $savemsg = get_std_save_message(true); + clear_subsystem_dirty('ipsec'); + header(url_safe('Location: /vpn_ipsec_mobile.php?savemsg=%s', array($savemsg))); + exit; + } elseif (isset($_POST['submit'])) { + // save form changes + if (!empty($pconfig['pool_address']) && !is_ipaddr($pconfig['pool_address'])) { + $input_errors[] = gettext("A valid IPv4 address for 'Virtual IPv4 Address Pool Network' must be specified."); + } + + if (!empty($pconfig['pool_address_v6']) && !is_ipaddr($pconfig['pool_address_v6'])) { + $input_errors[] = gettext("A valid IPv6 address for 'Virtual IPv6 Address Pool Network' must be specified."); + } + + + if (count($input_errors) == 0) { + $client = array(); + $copy_fields = "pool_address,pool_netbits,pool_address_v6,pool_netbits_v6"; + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!empty($pconfig[$fieldname])) { + $client[$fieldname] = $pconfig[$fieldname]; + } + } + if (!empty($pconfig['enable'])) { + $client['enable'] = true; + } + + $config['ipsec']['client'] = $client; + + write_config(); + mark_subsystem_dirty('ipsec'); + header(url_safe('Location: /vpn_ipsec_mobile.php')); + exit; + } + } + + // initialize missing post attributes + foreach (explode(",", $form_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } +} + +legacy_html_escape_form_data($pconfig); + +$service_hook = 'strongswan'; + +include("head.inc"); + +?> + + + + + + + +
        +
        +
        +" . gettext("You must apply the changes in order for them to take effect.")); +} +$ph1found = false; +$legacy_radius_configured = false; +foreach ($config['ipsec']['phase1'] as $ph1ent) { + if (!isset($ph1ent['disabled']) && isset($ph1ent['mobile'])) { + $ph1found = true; + if (($ph1ent['authentication_method'] ?? '') == 'eap-radius') { + $legacy_radius_configured = true; + } + } +} + +function print_legacy_box($msg, $name, $value) +{ + $savebutton = "
        "; + $savebutton .= ""; + if (!empty($_POST['if'])) { + $savebutton .= ""; + } + $savebutton .= '
        '; + + echo << + +
        + +EOFnp; +} + +if (!empty($pconfig['enable']) && !$ph1found && !(new OPNsense\IPsec\Swanctl())->isEnabled()) { + print_legacy_box(gettext("Support for IPsec Mobile clients is enabled but a Phase1 definition was not found") . ".
        " . gettext("When using (legacy) tunnels, please click Create to define one."), "create", gettext("Create Phase1")); +} +if (isset($input_errors) && count($input_errors) > 0) { + print_input_errors($input_errors); +} +?> +
        +
        +
        + + + + + + + + + + + + + + + + + + + + +
        + + +
        + /> + + +
        + onclick="pool_change()" /> + +
        + + +
        +
        + onclick="pool_v6_change()" /> + +
        + + +
        +
        +
        +
        +
        +
        + + + + + +
          + +
        +
        +
        +
        + +
        +
        +
        + + diff --git a/security/strongswan-legacy/src/www/vpn_ipsec_phase1.php b/security/strongswan-legacy/src/www/vpn_ipsec_phase1.php new file mode 100644 index 000000000..f2548491c --- /dev/null +++ b/security/strongswan-legacy/src/www/vpn_ipsec_phase1.php @@ -0,0 +1,1351 @@ + + * Copyright (C) 2014-2015 Deciso B.V. + * Copyright (C) 2008 Shrew Soft Inc. + * Copyright (C) 2003-2005 Manuel Kasper + * Copyright (C) 2014 Ermal Luçi + * 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. + */ + +require_once("guiconfig.inc"); +require_once("system.inc"); +require_once("filter.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/ipsec.inc"); + +/* + * ikeid management functions + */ + +function ipsec_ikeid_used($ikeid) { + global $config; + + if (!empty($config['ipsec']['phase1'])) { + foreach ($config['ipsec']['phase1'] as $ph1ent) { + if( $ikeid == $ph1ent['ikeid'] ) { + return true; + } + } + } + return false; +} + +function ipsec_ikeid_next() { + $ikeid = 1; + while(ipsec_ikeid_used($ikeid)) { + $ikeid++; + } + + return $ikeid; +} + +function ipsec_keypairs() +{ + $mdl = new \OPNsense\IPsec\IPsec(); + $node = $mdl->getNodeByReference('keyPairs.keyPair'); + + return $node ? $node->getNodes() : []; +} + +config_read_array('ipsec', 'phase1'); +config_read_array('ipsec', 'phase2'); + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // fetch data + if (isset($_GET['dup']) && is_numericint($_GET['dup'])) { + $p1index = $_GET['dup']; + } elseif (isset($_GET['p1index']) && is_numericint($_GET['p1index'])) { + $p1index = $_GET['p1index']; + } + $pconfig = array(); + + // generice defaults + $pconfig['interface'] = "wan"; + $pconfig['iketype'] = "ikev2"; + $phase1_fields = "mode,protocol,myid_type,myid_data,peerid_type,peerid_data + ,encryption-algorithm,lifetime,authentication_method,descr,nat_traversal,rightallowany,inactivity_timeout + ,interface,iketype,dpd_delay,dpd_maxfail,dpd_action,remote-gateway,pre-shared-key,certref,margintime,rekeyfuzz + ,caref,local-kpref,peer-kpref,reauth_enable,rekey_enable,auto,tunnel_isolation,authservers,mobike,keyingtries + ,closeaction,unique"; + if (isset($p1index) && isset($config['ipsec']['phase1'][$p1index])) { + // 1-on-1 copy + foreach (explode(",", $phase1_fields) as $fieldname) { + $fieldname = trim($fieldname); + if(isset($config['ipsec']['phase1'][$p1index][$fieldname])) { + $pconfig[$fieldname] = $config['ipsec']['phase1'][$p1index][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + + // attributes with some kind of logic behind them... + if (!isset($_GET['dup']) || !is_numericint($_GET['dup'])) { + // don't copy the ikeid on dup + $pconfig['ikeid'] = $config['ipsec']['phase1'][$p1index]['ikeid']; + } + $pconfig['disabled'] = isset($config['ipsec']['phase1'][$p1index]['disabled']); + $pconfig['sha256_96'] = !empty($config['ipsec']['phase1'][$p1index]['sha256_96']); + $pconfig['installpolicy'] = empty($config['ipsec']['phase1'][$p1index]['noinstallpolicy']); // XXX: reversed + + foreach (array('authservers', 'dhgroup', 'hash-algorithm') as $fieldname) { + if (!empty($config['ipsec']['phase1'][$p1index][$fieldname])) { + $pconfig[$fieldname] = explode(',', $config['ipsec']['phase1'][$p1index][$fieldname]); + } else { + $pconfig[$fieldname] = array(); + } + } + + $pconfig['remotebits'] = null; + $pconfig['remotenet'] = null ; + if (isset($a_phase1[$p1index]['remote-subnet']) && strpos($config['ipsec']['phase1'][$p1index]['remote-subnet'],'/') !== false) { + list($pconfig['remotenet'],$pconfig['remotebits']) = explode("/", $config['ipsec']['phase1'][$p1index]['remote-subnet']); + } elseif (isset($config['ipsec']['phase1'][$p1index]['remote-subnet'])) { + $pconfig['remotenet'] = $config['ipsec']['phase1'][$p1index]['remote-subnet']; + } + + if (isset($config['ipsec']['phase1'][$p1index]['mobile'])) { + $pconfig['mobile'] = true; + } + } else { + /* defaults new */ + if (isset($config['interfaces']['lan'])) { + $pconfig['localnet'] = "lan"; + } + $pconfig['mode'] = "main"; + $pconfig['protocol'] = "inet"; + $pconfig['myid_type'] = "myaddress"; + $pconfig['peerid_type'] = "peeraddress"; + $pconfig['authentication_method'] = "pre_shared_key"; + $pconfig['encryption-algorithm'] = ["name" => "aes256gcm16"]; + $pconfig['hash-algorithm'] = ['sha256']; + $pconfig['dhgroup'] = array('14'); + $pconfig['nat_traversal'] = "on"; + $pconfig['installpolicy'] = true; + $pconfig['authservers'] = array(); + + /* mobile client */ + if (isset($_GET['mobile'])) { + $pconfig['mobile'] = true; + } + // init empty + foreach (explode(",", $phase1_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + $a_phase1 = &config_read_array('ipsec', 'phase1'); + if (isset($_POST['p1index']) && is_numericint($_POST['p1index'])) { + $p1index = $_POST['p1index']; + } + $input_errors = array(); + $pconfig = $_POST; + $old_ph1ent = $a_phase1[$p1index]; + + // unset dpd on post + if (!isset($pconfig['dpd_enable'])) { + unset($pconfig['dpd_delay']); + unset($pconfig['dpd_maxfail']); + unset($pconfig['dpd_action']); + } + + /* My identity */ + if ($pconfig['myid_type'] == "myaddress") { + $pconfig['myid_data'] = ""; + } + /* Peer identity */ + if ($pconfig['myid_type'] == "peeraddress") { + $pconfig['peerid_data'] = ""; + } + + /* input validation */ + $method = $pconfig['authentication_method']; + + // Only require PSK here for normal PSK tunnels (not mobile) or xauth. + // For RSA methods, require the CA/Cert. + switch ($method) { + case "eap-tls": + case "psk_eap-tls": + case "eap-mschapv2": + case "rsa_eap-mschapv2": + case "eap-radius": + if (!in_array($pconfig['iketype'], array('ikev2', 'ike'))) { + $input_errors[] = sprintf(gettext("%s can only be used with IKEv2 type VPNs."), strtoupper($method)); + } + if ($method == 'eap-radius' && empty($pconfig['authservers'])) { + $input_errors[] = gettext("Please select radius servers to use."); + } + break; + case "pre_shared_key": + // If this is a mobile PSK tunnel the user PSKs go on + // the PSK tab, not here, so skip the check. + if ($pconfig['mobile']) { + break; + } + case "xauth_psk_server": + $reqdfields = explode(" ", "pre-shared-key"); + $reqdfieldsn = array(gettext("Pre-Shared Key")); + break; + case "hybrid_rsa_server": + $reqdfields = explode(' ', 'certref'); + $reqdfieldsn = array(gettext("Certificate")); + break; + case "xauth_rsa_server": + case "rsasig": + $reqdfields = explode(" ", "caref certref"); + $reqdfieldsn = array(gettext("Certificate Authority"),gettext("Certificate")); + break; + case "pubkey": + $reqdfields = explode(" ", "local-kpref peer-kpref"); + $reqdfieldsn = array(gettext("Local Key Pair"),gettext("Peer Key Pair")); + break; + } + + if (empty($pconfig['mobile'])) { + $reqdfields[] = "remote-gateway"; + $reqdfieldsn[] = gettext("Remote gateway"); + } + + do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); + + if (!empty($pconfig['inactivity_timeout']) && !is_numericint($pconfig['inactivity_timeout'])) { + $input_errors[] = gettext("The inactivity timeout must be an integer."); + } + if (!empty($pconfig['keyingtries']) && !is_numericint($pconfig['keyingtries']) && $pconfig['keyingtries'] != "-1") { + $input_errors[] = gettext("The keyingtries must be an integer."); + } + + if ((!empty($pconfig['lifetime']) && !is_numeric($pconfig['lifetime']))) { + $input_errors[] = gettext("The P1 lifetime must be an integer."); + } + if (!empty($pconfig['margintime'])) { + if (!is_numericint($pconfig['margintime'])) { + $input_errors[] = gettext("The margintime must be an integer."); + } else { + $rekeyfuzz = empty($pconfig['rekeyfuzz']) || !is_numeric($pconfig['rekeyfuzz']) ? 100 : $pconfig['rekeyfuzz']; + if (((int)$pconfig['margintime'] * 2) * ($rekeyfuzz / 100.0) > (int)$pconfig['lifetime']) { + $input_errors[] = gettext("The value margin... + margin... * rekeyfuzz must not exceed the original lifetime limit."); + } + } + } + if (!empty($pconfig['rekeyfuzz']) && !is_numericint($pconfig['rekeyfuzz'])) { + $input_errors[] = gettext("Rekeyfuzz must be an integer."); + } + + if (!empty($pconfig['remote-gateway'])) { + if (!is_ipaddr($pconfig['remote-gateway']) && !is_domain($pconfig['remote-gateway'])) { + $input_errors[] = gettext("A valid remote gateway address or host name must be specified."); + } elseif (is_ipaddrv4($pconfig['remote-gateway']) && ($pconfig['protocol'] != "inet")) { + $input_errors[] = gettext("A valid remote gateway IPv4 address must be specified or you need to change protocol to IPv6"); + } elseif (is_ipaddrv6($pconfig['remote-gateway']) && ($pconfig['protocol'] != "inet6")) { + $input_errors[] = gettext("A valid remote gateway IPv6 address must be specified or you need to change protocol to IPv4"); + } + } + + if (!empty($pconfig['remote-gateway']) && is_ipaddr($pconfig['remote-gateway']) && !isset($pconfig['disabled']) && + (empty($pconfig['iketype']) || $pconfig['iketype'] == "ikev1")) { + $t = 0; + foreach ($a_phase1 as $ph1tmp) { + if ($p1index != $t) { + if (isset($ph1tmp['remote-gateway']) && $ph1tmp['remote-gateway'] == $pconfig['remote-gateway'] && !isset($ph1tmp['disabled'])) { + $input_errors[] = sprintf(gettext('The remote gateway "%s" is already used by phase1 "%s".'), $pconfig['remote-gateway'], $ph1tmp['descr']); + } + } + $t++; + } + } + + if ($pconfig['interface'] == 'any' && $pconfig['myid_type'] == "myaddress") { + $input_errors[] = gettext("Please select an identifier (My Identifier) other then 'any' when selecting 'Any' interface"); + } elseif ($pconfig['myid_type'] == "address" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter an address for 'My Identifier'"); + } elseif ($pconfig['myid_type'] == "keyid tag" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter a keyid tag for 'My Identifier'"); + } elseif ($pconfig['myid_type'] == "fqdn" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter a fully qualified domain name for 'My Identifier'"); + } elseif ($pconfig['myid_type'] == "user_fqdn" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter a user and fully qualified domain name for 'My Identifier'"); + } elseif ($pconfig['myid_type'] == "dyn_dns" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter a dynamic domain name for 'My Identifier'"); + } elseif ((($pconfig['myid_type'] == "address") && !is_ipaddr($pconfig['myid_data']))) { + $input_errors[] = gettext("A valid IP address for 'My identifier' must be specified."); + } elseif ((($pconfig['myid_type'] == "fqdn") && !is_domain($pconfig['myid_data']))) { + $input_errors[] = gettext("A valid domain name for 'My identifier' must be specified."); + } elseif ($pconfig['myid_type'] == "fqdn" && !is_domain($pconfig['myid_data'])) { + $input_errors[] = gettext("A valid FQDN for 'My identifier' must be specified."); + } elseif ($pconfig['myid_type'] == "user_fqdn") { + $user_fqdn = explode("@", $pconfig['myid_data']); + if (is_domain($user_fqdn[1]) == false) { + $input_errors[] = gettext("A valid User FQDN in the form of user@my.domain.com for 'My identifier' must be specified."); + } + } elseif ($pconfig['myid_type'] == "dyn_dns") { + if (is_domain($pconfig['myid_data']) == false) { + $input_errors[] = gettext("A valid Dynamic DNS address for 'My identifier' must be specified."); + } + } + + // Only enforce peer ID if we are not dealing with a pure-psk mobile config. + if (!(($pconfig['authentication_method'] == "pre_shared_key") && !empty($pconfig['mobile']))) { + if ($pconfig['peerid_type'] == "address" and $pconfig['peerid_data'] == "") { + $input_errors[] = gettext("Please enter an address for 'Peer Identifier'"); + } + if ($pconfig['peerid_type'] == "keyid tag" and $pconfig['peerid_data'] == "") { + $input_errors[] = gettext("Please enter a keyid tag for 'Peer Identifier'"); + } + if ($pconfig['peerid_type'] == "fqdn" and $pconfig['peerid_data'] == "") { + $input_errors[] = gettext("Please enter a fully qualified domain name for 'Peer Identifier'"); + } + if ($pconfig['peerid_type'] == "user_fqdn" and $pconfig['peerid_data'] == "") { + $input_errors[] = gettext("Please enter a user and fully qualified domain name for 'Peer Identifier'"); + } + if ((($pconfig['peerid_type'] == "address") && !is_ipaddr($pconfig['peerid_data']))) { + $input_errors[] = gettext("A valid IP address for 'Peer identifier' must be specified."); + } + if ((($pconfig['peerid_type'] == "fqdn") && !is_domain($pconfig['peerid_data']))) { + $input_errors[] = gettext("A valid domain name for 'Peer identifier' must be specified."); + } + if ($pconfig['peerid_type'] == "fqdn") { + if (is_domain($pconfig['peerid_data']) == false) { + $input_errors[] = gettext("A valid FQDN for 'Peer identifier' must be specified."); + } + } + if ($pconfig['peerid_type'] == "user_fqdn") { + $user_fqdn = explode("@", $pconfig['peerid_data']); + if (is_domain($user_fqdn[1]) == false) { + $input_errors[] = gettext("A valid User FQDN in the form of user@my.domain.com for 'Peer identifier' must be specified."); + } + } + } + + if (!empty($pconfig['closeaction']) && !in_array($pconfig['closeaction'], ['clear', 'hold', 'restart'])) { + $input_errors[] = gettext('Invalid argument for close action.'); + } + + if (!empty($pconfig['unique']) && !in_array($pconfig['unique'], ['no', 'replace', 'never', 'keep'])) { + $input_errors[] = gettext('Invalid argument for unique.'); + } + + if (!empty($pconfig['dpd_enable'])) { + if (!is_numeric($pconfig['dpd_delay'])) { + $input_errors[] = gettext("A numeric value must be specified for DPD delay."); + } + if (!is_numeric($pconfig['dpd_maxfail'])) { + $input_errors[] = gettext("A numeric value must be specified for DPD retries."); + } + if (!empty($pconfig['dpd_action']) && !in_array($pconfig['dpd_action'], array("restart", "clear"))) { + $input_errors[] = gettext('Invalid argument for DPD action.'); + } + } + + if (!empty($pconfig['iketype']) && !in_array($pconfig['iketype'], array("ike", "ikev1", "ikev2"))) { + $input_errors[] = gettext('Invalid argument for key exchange protocol version.'); + } + + /* build our encryption algorithms array */ + if (!isset($pconfig['encryption-algorithm']) || !is_array($pconfig['encryption-algorithm'])) { + $pconfig['encryption-algorithm'] = array(); + } + $pconfig['encryption-algorithm']['name'] = $pconfig['ealgo']; + if (!empty($pconfig['ealgo_keylen'])) { + $pconfig['encryption-algorithm']['keylen'] = $pconfig['ealgo_keylen']; + } + + if (empty($pconfig['hash-algorithm'])) { + $input_errors[] = gettext("At least one hashing algorithm needs to be selected."); + $pconfig['hash-algorithm'] = array(); + } + + if (empty($pconfig['dhgroup'])) { + $pconfig['dhgroup'] = array(); + } + + foreach (ipsec_p1_ealgos() as $algo => $algodata) { + if (!empty($pconfig['iketype']) && !empty($pconfig['encryption-algorithm']['name']) && !empty($algodata['iketype']) + && $pconfig['iketype'] != $algodata['iketype'] && $pconfig['encryption-algorithm']['name'] == $algo) { + $input_errors[] = sprintf(gettext("%s can only be used with IKEv2 type VPNs."), $algodata['name']); + } + } + + if (!empty($pconfig['ikeid']) && !empty($pconfig['installpolicy'])) { + foreach ($config['ipsec']['phase2'] as $phase2ent) { + if ($phase2ent['ikeid'] == $pconfig['ikeid'] && $phase2ent['mode'] == 'route-based') { + $input_errors[] = gettext( + "Install policy on phase1 is not a valid option when using Route-based phase 2 entries." + ); + break; + } + } + } + + if (count($input_errors) == 0) { + $copy_fields = "ikeid,iketype,interface,mode,protocol,myid_type,myid_data + ,peerid_type,peerid_data,encryption-algorithm,margintime,rekeyfuzz,inactivity_timeout,keyingtries + ,lifetime,pre-shared-key,certref,caref,authentication_method,descr,local-kpref,peer-kpref + ,nat_traversal,auto,mobike,closeaction,unique"; + + foreach (explode(",",$copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if(!empty($pconfig[$fieldname])) { + $ph1ent[$fieldname] = $pconfig[$fieldname]; + } + } + + foreach (array('authservers', 'dhgroup', 'hash-algorithm') as $fieldname) { + if (!empty($pconfig[$fieldname])) { + $ph1ent[$fieldname] = implode(',', $pconfig[$fieldname]); + } + } + + $ph1ent['disabled'] = !empty($pconfig['disabled']); + $ph1ent['sha256_96'] = !empty($pconfig['sha256_96']); + $ph1ent['noinstallpolicy'] = empty($pconfig['installpolicy']); // XXX: reversed + $ph1ent['private-key'] =isset($pconfig['privatekey']) ? base64_encode($pconfig['privatekey']) : null; + if (!empty($pconfig['mobile'])) { + $ph1ent['mobile'] = true; + } else { + $ph1ent['remote-gateway'] = $pconfig['remote-gateway']; + } + if (isset($pconfig['reauth_enable'])) { + $ph1ent['reauth_enable'] = true; + } + if (isset($pconfig['rekey_enable'])) { + $ph1ent['rekey_enable'] = true; + } + + if (isset($pconfig['tunnel_isolation'])) { + $ph1ent['tunnel_isolation'] = true; + } + + if (isset($pconfig['rightallowany'])) { + $ph1ent['rightallowany'] = true; + } + + if (isset($pconfig['dpd_enable'])) { + $ph1ent['dpd_delay'] = $pconfig['dpd_delay']; + $ph1ent['dpd_maxfail'] = $pconfig['dpd_maxfail']; + $ph1ent['dpd_action'] = $pconfig['dpd_action']; + } + + /* generate unique phase1 ikeid */ + if ($ph1ent['ikeid'] == 0) { + $ph1ent['ikeid'] = ipsec_ikeid_next(); + } + + if (isset($p1index) && isset($a_phase1[$p1index])) { + $a_phase1[$p1index] = $ph1ent; + } else { + if (!empty($pconfig['clone_phase2']) && !empty($a_phase1[$_GET['dup']]) + && !empty($config['ipsec']['phase2'])) { + // clone phase 2 entries in disabled state if requested. + $prev_ike_id = $a_phase1[$_GET['dup']]['ikeid']; + foreach ($config['ipsec']['phase2'] as $phase2ent) { + if ($phase2ent['ikeid'] == $prev_ike_id) { + $new_phase2 = $phase2ent; + $new_phase2['disabled'] = true; + $new_phase2['uniqid'] = uniqid(); + $new_phase2['ikeid'] = $ph1ent['ikeid']; + unset($new_phase2['reqid']); + $config['ipsec']['phase2'][] = $new_phase2; + } + } + } + $a_phase1[] = $ph1ent; + } + + write_config(); + mark_subsystem_dirty('ipsec'); + + header(url_safe('Location: /ui/ipsec/tunnels')); + exit; + } +} + +$service_hook = 'strongswan'; + +legacy_html_escape_form_data($pconfig); +include("head.inc"); +?> + + + + + +
        +
        +
        + 0) { + print_input_errors($input_errors); + } +?> + +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + + +
        + /> + +
        + /> + + +
        + + + +
        + + + +
        + + +
        + + +
        + + +
        + /> + + +
        + + +
         
        + + +
        + + +
        + +
        + +
        +
        + + + + + +
        + " /> + +
        + + +
        + + +
        + + +
        + + +
        + + +
        + + + + +
        + + +
        + + +
        + /> + +
        + /> + +
        + /> + +
        + /> + +
        + /> + +
        + + +
        + /> + +
        + + +
        + + +
        + /> + +
        +
        + + + +
        + + + +
        + + + +
        +
        + + +
        + + +
        + + +
        + + +
        + + +
          + + + + + + + + +
        +
        +
        +
        +
        +
        +
        +
        + + + * Copyright (C) 2003-2005 Manuel Kasper + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/ipsec.inc"); + +/** + * combine ealgos and keylen_* tags + */ +function pconfig_to_ealgos($pconfig) +{ + $ealgos = []; + if (isset($pconfig['ealgos'])) { + foreach (ipsec_p2_ealgos() as $algo_name => $algo_data) { + if (in_array($algo_name, $pconfig['ealgos'])) { + $ealgos[] = ['name' => $algo_name]; + } + } + } + return $ealgos; +} + +function ealgos_to_pconfig(& $ealgos, & $pconfig) +{ + $p2_ealgos = ipsec_p2_ealgos(); + $pconfig['ealgos'] = []; + foreach ($ealgos as $cnf_algo_data) { + foreach ($p2_ealgos as $algo_name => $algo_data) { + if ($algo_name == $cnf_algo_data['name']) { + $pconfig['ealgos'][] = $algo_name; + } elseif ($algo_data['name'] == $cnf_algo_data['name']) { + // XXX: extract and convert legacy encryption-algorithm-option setting + if ($cnf_algo_data['keylen'] == $algo_data['keylen'] || $cnf_algo_data['keylen'] == "auto") { + $pconfig['ealgos'][] = $algo_name; + } + } + } + } + + return $ealgos; +} + +/** + * convert id_address, id_netbits, id_type + * to type/address/netbits structure + */ +function pconfig_to_idinfo($prefix, $pconfig) +{ + $type = isset($pconfig[$prefix."id_type"]) ? $pconfig[$prefix."id_type"] : null; + $address = isset($pconfig[$prefix."id_address"]) ? $pconfig[$prefix."id_address"] : null; + $netbits = isset($pconfig[$prefix."id_netbits"]) ? $pconfig[$prefix."id_netbits"] : null; + + switch ($type) { + case "address": + return array('type' => $type, 'address' => $address); + case "network": + return array('type' => $type, 'address' => $address, 'netbits' => $netbits); + default: + return array('type' => $type ); + } +} + +/** + * reverse pconfig_to_idinfo from $idinfo array to $pconfig + */ +function idinfo_to_pconfig($prefix, $idinfo, & $pconfig) +{ + switch ($idinfo['type']) { + case "address": + $pconfig[$prefix."id_type"] = $idinfo['type']; + $pconfig[$prefix."id_address"] = $idinfo['address']; + break; + case "network": + $pconfig[$prefix."id_type"] = $idinfo['type']; + $pconfig[$prefix."id_address"] = $idinfo['address']; + $pconfig[$prefix."id_netbits"] = $idinfo['netbits']; + break; + default: + $pconfig[$prefix."id_type"] = $idinfo['type']; + break; + } +} + +/** + * search phase 2 entries for record with uniqid + */ +function getIndexByUniqueId($uniqid) +{ + global $config; + $p2index = null; + if ($uniqid != null) { + foreach ($config['ipsec']['phase2'] as $idx => $ph2) { + if ($ph2['uniqid'] == $uniqid) { + $p2index = $idx; + break; + } + } + } + return $p2index; +} + +config_read_array('ipsec', 'client'); +config_read_array('ipsec', 'phase2'); + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // lookup p2index + if (!empty($_GET['dup'])) { + $p2index = getIndexByUniqueId($_GET['dup']); + } elseif (!empty($_GET['p2index'])) { + $p2index = getIndexByUniqueId($_GET['p2index']); + } else { + $p2index = null; + } + // initialize form data + $pconfig = array(); + + $phase2_fields = "ikeid,mode,descr,uniqid,proto,hash-algorithm-option,pfsgroup,lifetime,pinghost,protocol,spd,"; + $phase2_fields .= "tunnel_local,tunnel_remote"; + if ($p2index !== null) { + // 1-on-1 copy + foreach (explode(",", $phase2_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (isset($config['ipsec']['phase2'][$p2index][$fieldname])) { + $pconfig[$fieldname] = $config['ipsec']['phase2'][$p2index][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + // fields with some kind of logic + $pconfig['disabled'] = isset($config['ipsec']['phase2'][$p2index]['disabled']); + + idinfo_to_pconfig("local", $config['ipsec']['phase2'][$p2index]['localid'], $pconfig); + idinfo_to_pconfig("remote", $config['ipsec']['phase2'][$p2index]['remoteid'], $pconfig); + if (!empty($config['ipsec']['phase2'][$p2index]['encryption-algorithm-option'])) { + ealgos_to_pconfig($config['ipsec']['phase2'][$p2index]['encryption-algorithm-option'], $pconfig); + } else { + $pconfig['ealgos'] = []; + } + + if (!empty($_GET['dup'])) { + $pconfig['uniqid'] = uniqid(); + } + } else { + if (isset($_GET['ikeid'])) { + $pconfig['ikeid'] = $_GET['ikeid']; + } + /* defaults */ + $pconfig['localid_type'] = "lan"; + $pconfig['remoteid_type'] = "network"; + $pconfig['protocol'] = "esp"; + $pconfig['ealgos'] = ['aes256gcm16']; + $pconfig['hash-algorithm-option'] = ['hmac_sha256']; + $pconfig['pfsgroup'] = "0"; + $pconfig['uniqid'] = uniqid(); + + // init empty + foreach (explode(",", $phase2_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } + } + /* mobile client */ + foreach ($config['ipsec']['phase1'] as $phase1ent) { + if ($phase1ent['ikeid'] == $pconfig['ikeid'] && isset($phase1ent['mobile'])) { + $pconfig['mobile'] = true; + break; + } + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!empty($_POST['uniqid'])) { + $p2index = getIndexByUniqueId($_POST['uniqid']); + } else { + $p2index = null; + } + $input_errors = array(); + $pconfig = $_POST; + + /* input validation */ + if (!isset($_POST['ikeid'])) { + $input_errors[] = gettext("A valid ikeid must be specified."); + } + $reqdfields = explode(" ", "localid_type uniqid"); + $reqdfieldsn = array(gettext("Local network type"), gettext("Unique Identifier")); + if (!isset($pconfig['mobile'])) { + $reqdfields[] = "remoteid_type"; + $reqdfieldsn[] = gettext("Remote network type"); + } + + do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + + if (($pconfig['mode'] == 'tunnel') || ($pconfig['mode'] == 'tunnel6')) { + switch ($pconfig['localid_type']) { + case 'network': + if (($pconfig['localid_netbits'] != 0 && !$pconfig['localid_netbits']) || !is_numeric($pconfig['localid_netbits'])) { + $input_errors[] = gettext('A valid local network bit count must be specified.'); + } + /* FALLTHROUGH */ + case 'address': + if (!$pconfig['localid_address'] || !is_ipaddr($pconfig['localid_address'])) { + $input_errors[] = gettext('A valid local network IP address must be specified.'); + } elseif (is_ipaddrv4($pconfig['localid_address']) && ($pconfig['mode'] != 'tunnel')) { + $input_errors[] = gettext('A valid local network IPv4 address must be specified or you need to change Mode to IPv6'); + } elseif (is_ipaddrv6($pconfig['localid_address']) && ($pconfig['mode'] != 'tunnel6')) { + $input_errors[] = gettext('A valid local network IPv6 address must be specified or you need to change Mode to IPv4'); + } + break; + default: + if ($pconfig['mode'] == 'tunnel') { + list (, $subnet) = interfaces_primary_address($pconfig['localid_type']); + if (!is_subnetv4($subnet)) { + $input_errors[] = sprintf( + gettext('Invalid local network: %s has no valid IPv4 network.'), + convert_friendly_interface_to_friendly_descr($pconfig['localid_type']) + ); + } + } elseif ($pconfig['mode'] == 'tunnel6') { + list (, $subnet) = interfaces_primary_address6($pconfig['localid_type']); + if (!is_subnetv6($subnet)) { + $input_errors[] = sprintf( + gettext('Invalid local network: %s has no valid IPv6 network.'), + convert_friendly_interface_to_friendly_descr($pconfig['localid_type']) + ); + } + } + break; + } + + switch ($pconfig['remoteid_type']) { + case "network": + if (($pconfig['remoteid_netbits'] != 0 && !$pconfig['remoteid_netbits']) || !is_numeric($pconfig['remoteid_netbits'])) { + $input_errors[] = gettext("A valid remote network bit count must be specified."); + } + // address rules also apply to network type (hence, no break) + case "address": + if (!$pconfig['remoteid_address'] || !is_ipaddr($pconfig['remoteid_address'])) { + $input_errors[] = gettext("A valid remote network IP address must be specified."); + } elseif (is_ipaddrv4($pconfig['remoteid_address']) && ($pconfig['mode'] != "tunnel")) { + $input_errors[] = gettext("A valid remote network IPv4 address must be specified or you need to change Mode to IPv6"); + } elseif (is_ipaddrv6($pconfig['remoteid_address']) && ($pconfig['mode'] != "tunnel6")) { + $input_errors[] = gettext("A valid remote network IPv6 address must be specified or you need to change Mode to IPv4"); + } + break; + } + } elseif ($pconfig['mode'] == 'route-based') { + // validate if both tunnel networks are using the correct address family + if (!is_ipaddr($pconfig['tunnel_local']) || !is_ipaddr($pconfig['tunnel_remote'])) { + if (!is_ipaddr($pconfig['tunnel_local'])) { + $input_errors[] = gettext('A valid local network IP address must be specified.'); + } + if (!is_ipaddr($pconfig['tunnel_remote'])) { + $input_errors[] = gettext("A valid remote network IP address must be specified."); + } + } elseif( + !(is_ipaddrv4($pconfig['tunnel_local']) && is_ipaddrv4($pconfig['tunnel_remote'])) && + !(is_ipaddrv6($pconfig['tunnel_local']) && is_ipaddrv6($pconfig['tunnel_remote'])) + ) { + $input_errors[] = gettext("A valid local network IP address must be specified."); + $input_errors[] = gettext("A valid remote network IP address must be specified."); + } + } + /* Validate enabled phase2's are not duplicates */ + if (isset($pconfig['mobile'])) { + /* User is adding phase 2 for mobile phase1 */ + foreach ($config['ipsec']['phase2'] as $key => $name) { + if (isset($name['mobile']) && $pconfig['ikeid'] == $name['ikeid'] && $name['uniqid'] != $pconfig['uniqid']) { + /* check duplicate localids only for mobile clients */ + $localid_data = ipsec_idinfo_to_cidr($name['localid'], false, $name['mode']); + $entered = array(); + $entered['type'] = $pconfig['localid_type']; + if (isset($pconfig['localid_address'])) { + $entered['address'] = $pconfig['localid_address']; + } + if (isset($pconfig['localid_netbits'])) { + $entered['netbits'] = $pconfig['localid_netbits']; + } + $entered_localid_data = ipsec_idinfo_to_cidr($entered, false, $pconfig['mode']); + if ($localid_data == $entered_localid_data) { + /* adding new p2 entry */ + $input_errors[] = gettext("Phase2 with this Local Network is already defined for mobile clients."); + break; + } + } + } + } else { + /* User is adding phase 2 for site-to-site phase1 */ + foreach ($config['ipsec']['phase2'] as $key => $name) { + if (!isset($name['mobile']) && $pconfig['mode'] != 'route-based' && + $pconfig['ikeid'] == $name['ikeid'] && $pconfig['uniqid'] != $name['uniqid']) { + /* check duplicate subnets only for given phase1 */ + $localid_data = ipsec_idinfo_to_cidr($name['localid'], false, $name['mode']); + $remoteid_data = ipsec_idinfo_to_cidr($name['remoteid'], false, $name['mode']); + $entered_local = array(); + $entered_local['type'] = $pconfig['localid_type']; + if (isset($pconfig['localid_address'])) { + $entered_local['address'] = $pconfig['localid_address']; + } + if (isset($pconfig['localid_netbits'])) { + $entered_local['netbits'] = $pconfig['localid_netbits']; + } + $entered_localid_data = ipsec_idinfo_to_cidr($entered_local, false, $pconfig['mode']); + $entered_remote = array(); + $entered_remote['type'] = $pconfig['remoteid_type']; + if (isset($pconfig['remoteid_address'])) { + $entered_remote['address'] = $pconfig['remoteid_address']; + } + if (isset($pconfig['remoteid_netbits'])) { + $entered_remote['netbits'] = $pconfig['remoteid_netbits']; + } + $entered_remoteid_data = ipsec_idinfo_to_cidr($entered_remote, false, $pconfig['mode']); + if ($localid_data == $entered_localid_data && $remoteid_data == $entered_remoteid_data) { + /* adding new p2 entry */ + $input_errors[] = gettext("Phase2 with this Local/Remote networks combination is already defined for this Phase1."); + break; + } + } + } + } + + if (!empty($pconfig['ikeid'])) { + foreach ($config['ipsec']['phase1'] as $phase1ent) { + if ($phase1ent['ikeid'] == $pconfig['ikeid'] && + $pconfig['mode'] == 'route-based' && + empty($phase1ent['noinstallpolicy']) + ) { + $input_errors[] = gettext( + "Install policy on phase1 is not a valid option when using Route-based phase 2 entries." + ); + break; + } + } + } + + /* For ESP protocol, handle encryption algorithms */ + if ($pconfig['protocol'] == "esp") { + $ealgos = pconfig_to_ealgos($pconfig); + + if (!count($ealgos)) { + $input_errors[] = gettext("At least one encryption algorithm must be selected."); + } else { + if (empty($pconfig['hash-algorithm-option'])) { + foreach ($ealgos as $ealgo) { + if (!strpos($ealgo['name'], "gcm")) { + $input_errors[] = gettext("At least one hashing algorithm needs to be selected."); + break; + } + } + $pconfig['hash-algorithm-option'] = array(); + } + } + } + if ((!empty($_POST['lifetime']) && !is_numeric($_POST['lifetime']))) { + $input_errors[] = gettext("The P2 lifetime must be an integer."); + } + + if (!empty($pconfig['spd'])) { + foreach (explode(',', $pconfig['spd']) as $spd_entry) { + if (($pconfig['mode'] == "tunnel" && !is_subnetv4(trim($spd_entry))) || + ($pconfig['mode'] == "tunnel6" && !is_subnetv6(trim($spd_entry)))) { + $input_errors[] = sprintf(gettext('SPD "%s" is not a valid network, it should match the tunnel type (IPv4/IPv6).'), $spd_entry) ; + } + } + } + + if (count($input_errors) == 0) { + $ph2ent = array(); + $copy_fields = "ikeid,uniqid,mode,pfsgroup,lifetime,pinghost,descr,protocol,spd"; + + // 1-on-1 copy + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!empty($pconfig[$fieldname])) { + $ph2ent[$fieldname] = $pconfig[$fieldname]; + } + } + + // fields with some logic in them + $ph2ent['disabled'] = $pconfig['disabled'] ? true : false; + if (($ph2ent['mode'] == "tunnel") || ($ph2ent['mode'] == "tunnel6")) { + $ph2ent['localid'] = pconfig_to_idinfo("local", $pconfig); + $ph2ent['remoteid'] = pconfig_to_idinfo("remote", $pconfig); + } elseif ($ph2ent['mode'] == 'route-based') { + $ph2ent['tunnel_local'] = $pconfig['tunnel_local']; + $ph2ent['tunnel_remote'] = $pconfig['tunnel_remote']; + } + + $ph2ent['encryption-algorithm-option'] = pconfig_to_ealgos($pconfig); + + if (!empty($pconfig['hash-algorithm-option'])) { + $ph2ent['hash-algorithm-option'] = $pconfig['hash-algorithm-option']; + } else { + unset($ph2ent['hash-algorithm-option']); + } + + // attach or generate reqid + if ($p2index !== null && !empty($config['ipsec']['phase2'][$p2index]['reqid'])) { + $ph2ent['reqid'] = $config['ipsec']['phase2'][$p2index]['reqid']; + } else { + $reqids = []; + foreach ($config['ipsec']['phase2'] as $tmp) { + if (!empty($tmp['reqid'])) { + $reqids[] = $tmp['reqid']; + } + } + for ($i=1; $i < 65535; $i++) { + if (!in_array($i, $reqids)) { + $ph2ent['reqid'] = $i; + break; + } + } + } + // save to config + if ($p2index !== null) { + $config['ipsec']['phase2'][$p2index] = $ph2ent; + } else { + $config['ipsec']['phase2'][] = $ph2ent; + } + + + write_config(); + mark_subsystem_dirty('ipsec'); + + header(url_safe('Location: /ui/ipsec/tunnels')); + exit; + } +} + +$service_hook = 'strongswan'; + +legacy_html_escape_form_data($pconfig); + +include("head.inc"); + +?> + + + + + + 0) { + print_input_errors($input_errors); +} +?> +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + + +
        + /> + +
        + +
        + + +
        + +
        + +
        + +
           + + + + + +
        + + + +
        +
        :   + +
        :   + + + + + +
        + + + +
        +
        + +
        + +
        + +
        + +
        + + +
        + + + + + +
        + + +
        + + +
        + +
        + + +
        + + +
          + + + + + + + +
        +
        +
        +
        +
        +
        +
        +
        + + diff --git a/security/stunnel/Makefile b/security/stunnel/Makefile index a7e42ae0d..5de6a72ff 100644 --- a/security/stunnel/Makefile +++ b/security/stunnel/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= stunnel -PLUGIN_VERSION= 1.0.5 -PLUGIN_REVISION= 3 +PLUGIN_VERSION= 1.0.6 +PLUGIN_REVISION= 1 PLUGIN_COMMENT= Stunnel TLS proxy PLUGIN_MAINTAINER= ad@opnsense.org PLUGIN_DEPENDS= stunnel diff --git a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.xml b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.xml index 47970192d..0b851ec5d 100644 --- a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.xml +++ b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.xml @@ -1,6 +1,6 @@ //OPNsense/Stunnel - 1.0.3 + 1.0.4 Stunnel TLS encryption proxy @@ -48,6 +48,8 @@ IMAP + LDAP + NNTP POP3 SMTP @@ -72,8 +74,6 @@ Y Y stunnel ssl ciphers - /tmp/stunnel_ciphers_list.json - 360 Please specify valid tls ciphers. @@ -83,7 +83,7 @@ N - /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){0,255}$/u + /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){0,255}$/u Description should be a string between 1 and 255 characters diff --git a/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt b/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt index fe9967c40..18e1f479a 100644 --- a/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt +++ b/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt @@ -27,12 +27,12 @@ + +
        + {{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_GeneralSettings'])}} +
        + +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/beats/service/reconfigure'}) }} diff --git a/sysutils/beats/src/opnsense/service/conf/actions.d/actions_beats.conf b/sysutils/beats/src/opnsense/service/conf/actions.d/actions_beats.conf new file mode 100644 index 000000000..4fb0ea897 --- /dev/null +++ b/sysutils/beats/src/opnsense/service/conf/actions.d/actions_beats.conf @@ -0,0 +1,24 @@ +[start] +command:/usr/local/etc/rc.d/filebeat start +parameters: +type:script +message:starting Filebeat + +[stop] +command:/usr/local/etc/rc.d/filebeat stop +parameters: +type:script +message:stopping Filebeat + +[restart] +command:/usr/local/etc/rc.d/filebeat restart +parameters: +type:script +message:restarting Filebeat + +[status] +command:/usr/local/etc/rc.d/filebeat status +parameters: +errors:no +type:script_output +message:requesting Filebeat status diff --git a/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/+TARGETS b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/+TARGETS new file mode 100644 index 000000000..408300f17 --- /dev/null +++ b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/+TARGETS @@ -0,0 +1,2 @@ +filebeat.yml:/usr/local/etc/beats/filebeat.yml +filebeat:/etc/rc.conf.d/filebeat diff --git a/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat new file mode 100755 index 000000000..c3a61aa37 --- /dev/null +++ b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat @@ -0,0 +1 @@ +filebeat_enable="{{ 'YES' if not helpers.empty('OPNsense.filebeat.enabled') else 'NO' }}" diff --git a/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat.yml b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat.yml new file mode 100644 index 000000000..cf893f1e2 --- /dev/null +++ b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat.yml @@ -0,0 +1,460 @@ +######################## Filebeat Configuration ############################ + +#========================== Modules configuration ============================= +{% set filebeat_modules_enabled = (OPNsense.filebeat.modules.enabled|default('')).split(',') %} +filebeat.modules: +{% if 'suricata' in filebeat_modules_enabled %} +#-------------------------------- Suricata Module -------------------------------- +- module: suricata + # EVE + eve: + enabled: true + + # Set custom paths for the log files. If left empty, + # Filebeat will choose the paths depending on your OS. + #var.paths: + + # Internal network configuration (advanced) can be added under this section. + #var.internal_networks: +{% endif %} + + +#=========================== Filebeat inputs ============================= + +# List of inputs to fetch data. +{% set filebeat_inputs_enabled = (OPNsense.filebeat.inputs.enabled|default('')).split(',') %} +filebeat.inputs: +# Each - is an input. Most options can be set at the input level, so +# you can use different inputs for various configurations. +# Below are the input specific configurations. + +# Type of the files. Based on this the way the file is read is decided. +# The different types cannot be mixed in one input +# +# Possible options are: +# * filestream: Reads every line of the log file +# * log: Reads every line of the log file (deprecated) +# * stdin: Reads the standard in + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: audit + tags: ['audit'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'audit' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/audit/audit_*.log + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: configd + tags: ['configd'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'configd' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/configd/configd_*.log + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: 'boot' + tags: ['boot'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'boot' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/boot.log + + close.reader.on_eof: true + prospector: + scanner: + resend_on_touch: true + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: 'system' + tags: ['system'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'system' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/system/system_*.log + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: 'lighttpd' + tags: ['lighttpd'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'lighttpd' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/lighttpd/lighttpd_*.log + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +# ================================== Outputs =================================== + +# Configure what output to use when sending the data collected by the beat. + +# ---------------------------- Elasticsearch Output ---------------------------- +output.elasticsearch: + # Boolean flag to enable or disable the output module. + #enabled: true + + # Array of hosts to connect to. + # Scheme and port can be left out and will be set to the default (http and 9200) + # In case you specify and additional path, the scheme is required: http://localhost:9200/path + # IPv6 addresses should always be defined as: https://[2001:db8::1]:9200 + hosts: ["{{ OPNsense.filebeat.output.elasticsearch.hosts }}"] + + # Performance presets configure other output fields to recommended values + # based on a performance priority. + # Options are "balanced", "throughput", "scale", "latency" and "custom". + # Default if unspecified: "custom" + preset: balanced + + # Set gzip compression level. Set to 0 to disable compression. + # This field may conflict with performance presets. To set it + # manually use "preset: custom". + # The default is 1. + #compression_level: 1 + + # Configure escaping HTML symbols in strings. + #escape_html: false + + # Protocol - either `http` (default) or `https`. + #protocol: "https" + + # Authentication credentials - either API key or username/password. + api_key: "{{ OPNsense.filebeat.output.elasticsearch.api_key }}" + #username: "elastic" + #password: "changeme" + + # Dictionary of HTTP parameters to pass within the URL with index operations. + #parameters: + #param1: value1 + #param2: value2 + + # Number of workers per Elasticsearch host. + # This field may conflict with performance presets. To set it + # manually use "preset: custom". + #worker: 1 + + # If set to true and multiple hosts are configured, the output plugin load + # balances published events onto all Elasticsearch hosts. If set to false, + # the output plugin sends all events to only one host (determined at random) + # and will switch to another host if the currently selected one becomes + # unreachable. The default value is true. + #loadbalance: true + + # Optional data stream or index name. The default is "filebeat-%{[agent.version]}". + # In case you modify this pattern you must update setup.template.name and setup.template.pattern accordingly. + #index: "filebeat-%{[agent.version]}" + + # Optional ingest pipeline. By default, no pipeline will be used. + #pipeline: "" + + # Optional HTTP path + #path: "/elasticsearch" + + # Custom HTTP headers to add to each request + #headers: + # X-My-Header: Contents of the header + + # Proxy server URL + #proxy_url: http://proxy:3128 + + # Whether to disable proxy settings for outgoing connections. If true, this + # takes precedence over both the proxy_url field and any environment settings + # (HTTP_PROXY, HTTPS_PROXY). The default is false. + #proxy_disable: false + + # The number of times a particular Elasticsearch index operation is attempted. If + # the indexing operation doesn't succeed after this many retries, the events are + # dropped. The default is 3. + #max_retries: 3 + + # The maximum number of events to bulk in a single Elasticsearch bulk API index request. + # This field may conflict with performance presets. To set it + # manually use "preset: custom". + # The default is 1600. + #bulk_max_size: 1600 + + # The number of seconds to wait before trying to reconnect to Elasticsearch + # after a network error. After waiting backoff.init seconds, the Beat + # tries to reconnect. If the attempt fails, the backoff timer is increased + # exponentially up to backoff.max. After a successful connection, the backoff + # timer is reset. The default is 1s. + #backoff.init: 1s + + # The maximum number of seconds to wait before attempting to connect to + # Elasticsearch after a network error. The default is 60s. + #backoff.max: 60s + + # The maximum amount of time an idle connection will remain idle + # before closing itself. Zero means use the default of 60s. The + # format is a Go language duration (example 60s is 60 seconds). + # This field may conflict with performance presets. To set it + # manually use "preset: custom". + # The default is 3s. + # idle_connection_timeout: 3s + + # Configure HTTP request timeout before failing a request to Elasticsearch. + #timeout: 90 + + # Prevents filebeat from connecting to older Elasticsearch versions when set to `false` + #allow_older_versions: true + + # Use SSL settings for HTTPS. + #ssl.enabled: true + + # Controls the verification of certificates. Valid values are: + # * full, which verifies that the provided certificate is signed by a trusted + # authority (CA) and also verifies that the server's hostname (or IP address) + # matches the names identified within the certificate. + # * strict, which verifies that the provided certificate is signed by a trusted + # authority (CA) and also verifies that the server's hostname (or IP address) + # matches the names identified within the certificate. If the Subject Alternative + # Name is empty, it returns an error. + # * certificate, which verifies that the provided certificate is signed by a + # trusted authority (CA), but does not perform any hostname verification. + # * none, which performs no verification of the server's certificate. This + # mode disables many of the security benefits of SSL/TLS and should only be used + # after very careful consideration. It is primarily intended as a temporary + # diagnostic mechanism when attempting to resolve TLS errors; its use in + # production environments is strongly discouraged. + # The default value is full. + ssl.verification_mode: {{ OPNsense.filebeat.output.elasticsearch.ssl.verification_mode|default('full') }} + + # List of supported/valid TLS versions. By default all TLS versions from 1.1 + # up to 1.3 are enabled. + #ssl.supported_protocols: [TLSv1.1, TLSv1.2, TLSv1.3] + + # List of root certificates for HTTPS server verifications + #ssl.certificate_authorities: ["/etc/pki/root/ca.pem"] + + # Certificate for SSL client authentication + #ssl.certificate: "/etc/pki/client/cert.pem" + + # Client certificate key + #ssl.key: "/etc/pki/client/cert.key" + + # Optional passphrase for decrypting the certificate key. + #ssl.key_passphrase: '' + + # Configure cipher suites to be used for SSL connections + #ssl.cipher_suites: [] + + # Configure curve types for ECDHE-based cipher suites + #ssl.curve_types: [] + + # Configure what types of renegotiation are supported. Valid options are + # never, once, and freely. Default is never. + #ssl.renegotiation: never + + # Configure a pin that can be used to do extra validation of the verified certificate chain, + # this allow you to ensure that a specific certificate is used to validate the chain of trust. + # + # The pin is a base64 encoded string of the SHA-256 fingerprint. + #ssl.ca_sha256: "" + + # A root CA HEX encoded fingerprint. During the SSL handshake if the + # fingerprint matches the root CA certificate, it will be added to + # the provided list of root CAs (`certificate_authorities`), if the + # list is empty or not defined, the matching certificate will be the + # only one in the list. Then the normal SSL validation happens. +{% if not helpers.empty('OPNsense.filebeat.output.elasticsearch.ssl.ca_trusted_fingerprint') %} + ssl.ca_trusted_fingerprint: "{{ OPNsense.filebeat.output.elasticsearch.ssl.ca_trusted_fingerprint|replace(':','') }}" +{% else %} + #ssl.ca_trusted_fingerprint: "" +{% endif %} + + # Enables restarting filebeat if any file listed by `key`, + # `certificate`, or `certificate_authorities` is modified. + # This feature IS NOT supported on Windows. + #ssl.restart_on_cert_change.enabled: false + + # Period to scan for changes on CA certificate files + #ssl.restart_on_cert_change.period: 1m + + # Enable Kerberos support. Kerberos is automatically enabled if any Kerberos setting is set. + #kerberos.enabled: true + + # Authentication type to use with Kerberos. Available options: keytab, password. + #kerberos.auth_type: password + + # Path to the keytab file. It is used when auth_type is set to keytab. + #kerberos.keytab: /etc/elastic.keytab + + # Path to the Kerberos configuration. + #kerberos.config_path: /etc/krb5.conf + + # Name of the Kerberos user. + #kerberos.username: elastic + + # Password of the Kerberos user. It is used when auth_type is set to password. + #kerberos.password: changeme + + # Kerberos realm. + #kerberos.realm: ELASTIC + + +# ================================== Logging =================================== + +# There are four options for the log output: file, stderr, syslog, eventlog +# The file output is the default. + +# Sets log level. The default log level is info. +# Available log levels are: error, warning, info, debug +#logging.level: info + +# Enable debug output for selected components. To enable all selectors use ["*"] +# Other available selectors are "beat", "publisher", "service" +# Multiple selectors can be chained. +#logging.selectors: [ ] + +# Send all logging output to stderr. The default is false. +#logging.to_stderr: false + +# Send all logging output to syslog. The default is false. +logging.to_syslog: true + +# Send all logging output to Windows Event Logs. The default is false. +#logging.to_eventlog: false + +# If enabled, Filebeat periodically logs its internal metrics that have changed +# in the last period. For each metric that changed, the delta from the value at +# the beginning of the period is logged. Also, the total values for +# all non-zero internal metrics are logged on shutdown. The default is true. +# This is disabled on FreeBSD due to procfs not providing /proc/curproc/stat +logging.metrics.enabled: false + +# The period after which to log the internal metrics. The default is 30s. +#logging.metrics.period: 30s + +# A list of metrics namespaces to report in the logs. Defaults to [stats]. +# `stats` contains general Beat metrics. `dataset` may be present in some +# Beats and contains module or input metrics. +#logging.metrics.namespaces: [stats] + +# Logging to rotating files. Set logging.to_files to false to disable logging to +# files. +logging.to_files: false +logging.files: + # Configure the path where the logs are written. The default is the logs directory + # under the home path (the binary location). + #path: /var/log/filebeat + + # The name of the files where the logs are written to. + #name: filebeat + + # Configure log file size limit. If the limit is reached, log file will be + # automatically rotated. + #rotateeverybytes: 10485760 # = 10MB + + # Number of rotated log files to keep. The oldest files will be deleted first. + #keepfiles: 7 + + # The permissions mask to apply when rotating log files. The default value is 0600. + # Must be a valid Unix-style file permissions mask expressed in octal notation. + #permissions: 0600 + + # Enable log file rotation on time intervals in addition to the size-based rotation. + # Intervals must be at least 1s. Values of 1m, 1h, 24h, 7*24h, 30*24h, and 365*24h + # are boundary-aligned with minutes, hours, days, weeks, months, and years as + # reported by the local system clock. All other intervals are calculated from the + # Unix epoch. Defaults to disabled. + #interval: 0 + + # Rotate existing logs on startup rather than appending them to the existing + # file. Defaults to true. + # rotateonstartup: true diff --git a/sysutils/dmidecode/Makefile b/sysutils/dmidecode/Makefile index 428d1621a..590aef8fa 100644 --- a/sysutils/dmidecode/Makefile +++ b/sysutils/dmidecode/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= dmidecode -PLUGIN_VERSION= 1.1 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.2 PLUGIN_COMMENT= Display hardware information on the dashboard PLUGIN_DEPENDS= dmidecode PLUGIN_MAINTAINER= evbevz@gmail.com diff --git a/sysutils/dmidecode/src/opnsense/mvc/app/controllers/OPNsense/Dmidecode/Api/ServiceController.php b/sysutils/dmidecode/src/opnsense/mvc/app/controllers/OPNsense/Dmidecode/Api/ServiceController.php new file mode 100644 index 000000000..6dc1ed86e --- /dev/null +++ b/sysutils/dmidecode/src/opnsense/mvc/app/controllers/OPNsense/Dmidecode/Api/ServiceController.php @@ -0,0 +1,42 @@ +configdRun('dmidecode system')), false, INI_SCANNER_RAW); + $bios = parse_ini_string(trim((new Backend())->configdRun('dmidecode bios')), false, INI_SCANNER_RAW); + return ['status' => 'ok', 'system' => $system, 'bios' => $bios]; + } +} diff --git a/sysutils/dmidecode/src/opnsense/mvc/app/models/OPNsense/Dmidecode/ACL/ACL.xml b/sysutils/dmidecode/src/opnsense/mvc/app/models/OPNsense/Dmidecode/ACL/ACL.xml new file mode 100644 index 000000000..d5ef40eb1 --- /dev/null +++ b/sysutils/dmidecode/src/opnsense/mvc/app/models/OPNsense/Dmidecode/ACL/ACL.xml @@ -0,0 +1,8 @@ + + + Service: DMI Data Widget + + api/dmidecode/service/get + + + diff --git a/sysutils/dmidecode/src/opnsense/www/js/widgets/Dmidecode.js b/sysutils/dmidecode/src/opnsense/www/js/widgets/Dmidecode.js new file mode 100644 index 000000000..32a7dbcd7 --- /dev/null +++ b/sysutils/dmidecode/src/opnsense/www/js/widgets/Dmidecode.js @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2025 Neil Merchant + * 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. + */ + +export default class Dmidecode extends BaseTableWidget { + constructor() { + super(); + this.title = 'DMI Data'; + } + + getMarkup() { + let $container = $('
        '); + // make table for system output, add header + let $system_table = super.createTable('system-table', { headerPosition: 'left' }); + $container.append(`

        ${this.translations.system}

        `) + $container.append($system_table); + // same for bios output + let $bios_table = super.createTable('bios-table', { headerPosition: 'left' }); + $container.append(`

        ${this.translations.bios}

        `) + $container.append($bios_table); + return $container; + } + + async onMarkupRendered() { + const dmiData = await this.ajaxCall('/api/dmidecode/service/get'); + if (!dmiData || dmiData?.status !== 'ok') { + this.displayError('dmi lookup failed'); + return; + } + this.processDMIData(dmiData); + } + + processDMIData(data) { + const sysrows = []; + for (const [key, value] of Object.entries(data.system)) { + const row = []; + // try to find translation for key, fallback to output value + // have to split on spaces here because those aren't valid in xml tags + const translationIndex = key.split(" ")[0] + const dispKey = this.translations[translationIndex] || key + row.push(`
        ${dispKey}
        `, `
        ${value}
        `); + sysrows.push(row); + } + const biosrows = []; + for (const [key, value] of Object.entries(data.bios)) { + const row = []; + // try to find translation for key, fallback to output value + // have to split on spaces here because those aren't valid in xml tags + const translationIndex = key.split(" ")[0] + const dispKey = this.translations[translationIndex] || key + row.push(`
        ${dispKey}
        `, `
        ${value}
        `); + biosrows.push(row); + } + super.updateTable('system-table', sysrows); + super.updateTable('bios-table', biosrows); + } + + displayError(message) { + // if something went wrong, display error message in system table + const $error = $(` +
        + ${message} +
        + `); + $('#system-table').empty().append($error); +} + +} diff --git a/sysutils/dmidecode/src/opnsense/www/js/widgets/Metadata/Dmidecode.xml b/sysutils/dmidecode/src/opnsense/www/js/widgets/Metadata/Dmidecode.xml new file mode 100644 index 000000000..8bc482bec --- /dev/null +++ b/sysutils/dmidecode/src/opnsense/www/js/widgets/Metadata/Dmidecode.xml @@ -0,0 +1,20 @@ + + + Dmidecode.js + + /api/dmidecode/service/get + + + DMI Data + Platform + BIOS + Manufacturer + Product Name + Version + Serial Number + Family + Vendor + Release Date + + + diff --git a/sysutils/dmidecode/src/www/widgets/include/dmidecode.inc b/sysutils/dmidecode/src/www/widgets/include/dmidecode.inc deleted file mode 100644 index 0fd5de9f6..000000000 --- a/sysutils/dmidecode/src/www/widgets/include/dmidecode.inc +++ /dev/null @@ -1,3 +0,0 @@ - gettext('Backup - Google Drive'), + 'section' => 'system.remotebackup', + 'id' => 'remotebackup', + ]]; +} diff --git a/sysutils/gdrive-backup/src/opnsense/mvc/app/library/Google/API/Drive.php b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/Google/API/Drive.php new file mode 100644 index 000000000..ef1ffb437 --- /dev/null +++ b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/Google/API/Drive.php @@ -0,0 +1,148 @@ +client = new \Google_Client(); + + $service_account = [ + "type" => "service_account", + "private_key" => $certinfo['pkey'], + "client_email" => $client_id, + "client_id" => $client_id, + "auth_uri" => "https://accounts.google.com/o/oauth2/auth", + "token_uri" => "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url" => "https://www.googleapis.com/oauth2/v1/certs" + ]; + + $this->client->setAuthConfig($service_account); + $this->client->addScope("https://www.googleapis.com/auth/drive"); + $this->client->setApplicationName("OPNsense"); + + $this->service = new \Google_Service_Drive($this->client); + } + + /** + * retrieve directory listing + * @param $directoryId parent directory id + * @param $filename title/filename of object + * @return mixed list of files + */ + public function listFiles($directoryId, $filename = null) + { + $query = "'" . $directoryId . "' in parents "; + if ($filename != null) { + $query .= " and title in '" . $filename . "'"; + } + return $this->service->files->listFiles(['q' => $query, 'supportsAllDrives' => true]); + } + + + /** + * download a file by given GDrive file handle + * @param $fileHandle (object from listFiles) + * @return null|string + */ + public function download($fileHandle) + { + $response = $this->service->files->get($fileHandle->id, ['alt' => 'media', 'supportsAllDrives' => true]); + return $response->getBody()->getContents(); + } + + /** + * Upload file + * @param string $directoryId (parent id) + * @param string $filename + * @param string $content + * @param string $mimetype + * @return \Google_Service_Drive_DriveFile handle + */ + public function upload($directoryId, $filename, $content, $mimetype = 'text/plain') + { + + $file = new \Google_Service_Drive_DriveFile(); + $file->setName($filename); + $file->setDescription($filename); + $file->setMimeType('text/plain'); + $file->setParents([$directoryId]); + + $createdFile = $this->service->files->create($file, [ + 'data' => $content, + 'mimeType' => $mimetype, + 'uploadType' => 'media', + 'supportsAllDrives' => true + ]); + + return $createdFile; + } + + /** + * delete file + * @param $fileHandle (object from listFiles) + */ + public function delete($fileHandle) + { + $this->service->files->delete($fileHandle['id'], ['supportsAllDrives' => true]); + } +} diff --git a/sysutils/gdrive-backup/src/opnsense/mvc/app/library/OPNsense/Backup/GDrive.php b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/OPNsense/Backup/GDrive.php new file mode 100644 index 000000000..089ff08cd --- /dev/null +++ b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/OPNsense/Backup/GDrive.php @@ -0,0 +1,306 @@ + "GDriveEnabled", + "type" => "checkbox", + "label" => gettext("Enable"), + "value" => null + ); + $fields[] = array( + "name" => "GDriveEmail", + "type" => "text", + "label" => gettext("Email Address"), + "help" => gettext("Client-ID in the Google cloud console"), + "value" => null + ); + $fields[] = array( + "name" => "GDriveP12key", + "type" => "file", + "label" => gettext("P12 key"), + "help" => sprintf( + gettext('You need a private key in p12 format to use Google Drive, ' . + 'instructions on how to acquire one can be found %shere%s.'), + '', + '' + ), + "value" => null + ); + $fields[] = array( + "name" => "GDriveFolderID", + "type" => "text", + "label" => gettext("Folder ID"), + "value" => null + ); + $fields[] = array( + "name" => "GDrivePrefixHostname", + "type" => "checkbox", + "label" => gettext("Prefix hostname to backupfile"), + "help" => gettext("Normally the config xml will be written as config-stamp.xml, with this option set " . + "the filename will use the systems host and domain name."), + "value" => null + ); + $fields[] = array( + "name" => "GDriveBackupCount", + "type" => "text", + "label" => gettext("Backup Count"), + "value" => 60 + ); + $fields[] = array( + "name" => "GDrivePassword", + "type" => "password", + "label" => gettext("Password"), + "value" => null + ); + $fields[] = array( + "name" => "GDrivePasswordConfirm", + "type" => "password", + "label" => gettext("Confirm"), + "value" => null + ); + $cnf = Config::getInstance(); + if ($cnf->isValid()) { + $config = $cnf->object(); + foreach ($fields as &$field) { + $fieldname = $field['name']; + if (isset($config->system->remotebackup->$fieldname)) { + $field['value'] = (string)$config->system->remotebackup->$fieldname; + } elseif ( + $fieldname == "GDrivePasswordConfirm" && + isset($config->system->remotebackup->GDrivePassword) + ) { + $field['value'] = (string)$config->system->remotebackup->GDrivePassword; + } + } + } + + return $fields; + } + + /** + * backup provider name + * @return string user friendly name + */ + public function getName() + { + return gettext("Google Drive"); + } + + /** + * validate and set configuration + * @param array $conf configuration array + * @return array of validation errors when not saved + */ + public function setConfiguration($conf) + { + $input_errors = array(); + if ($conf['GDrivePasswordConfirm'] != $conf['GDrivePassword']) { + $input_errors[] = gettext("The supplied 'Password' and 'Confirm' field values must match."); + } + if (count($input_errors) == 0) { + $config = Config::getInstance()->object(); + if (!isset($config->system->remotebackup)) { + $config->system->addChild('remotebackup'); + } + foreach ($this->getConfigurationFields() as $field) { + $fieldname = $field['name']; + if ($field['type'] == 'file') { + if (!empty($conf[$field['name']])) { + $config->system->remotebackup->$fieldname = base64_encode($conf[$field['name']]); + } + } elseif ($field['name'] == 'GDrivePasswordConfirm') { + /* skip password confirm field */ + } elseif (!empty($conf[$field['name']])) { + $config->system->remotebackup->$fieldname = $conf[$field['name']]; + } else { + unset($config->system->remotebackup->$fieldname); + } + } + // remove private key when disabled + if ( + empty($config->system->remotebackup->GDriveEnabled) && + isset($config->system->remotebackup->GDriveP12key) + ) { + unset($config->system->remotebackup->GDriveP12key); + } + Config::getInstance()->save(); + } + + return $input_errors; + } + + /** + * @return array filelist + */ + public function backup() + { + $cnf = Config::getInstance(); + if ($cnf->isValid()) { + $config = $cnf->object(); + if ( + isset($config->system->remotebackup) && isset($config->system->remotebackup->GDriveEnabled) + && !empty($config->system->remotebackup->GDriveEnabled) + ) { + if (!empty($config->system->remotebackup->GDrivePrefixHostname)) { + $fileprefix = (string)$config->system->hostname . "." . (string)$config->system->domain . "-"; + } else { + $fileprefix = "config-"; + } + try { + $client = new \Google\API\Drive(); + $client->login( + (string)$config->system->remotebackup->GDriveEmail, + (string)$config->system->remotebackup->GDriveP12key + ); + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "error connecting to Google Drive"); + return array(); + } + + // backup source data to local strings (plain/encrypted) + $confdata = file_get_contents('/conf/config.xml'); + $confdata_enc = $this->encrypt($confdata, (string)$config->system->remotebackup->GDrivePassword); + + // read filelist ({prefix}*.xml) + try { + $files = $client->listFiles((string)$config->system->remotebackup->GDriveFolderID); + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "error while fetching filelist from Google Drive"); + return array(); + } + + $configfiles = array(); + foreach ($files as $file) { + if (fnmatch("{$fileprefix}*.xml", $file['name'])) { + $configfiles[$file['name']] = $file; + } + } + krsort($configfiles); + + + // backup new file if changed (or if first in backup) + $target_filename = $fileprefix . time() . ".xml"; + if (count($configfiles) > 1) { + // compare last backup with current, only save new + try { + $bck_data_enc = $client->download($configfiles[array_keys($configfiles)[0]]); + if (strpos(substr($bck_data_enc, 0, 100), '---') !== false) { + // base64 string is wrapped into tags + $start_at = strpos($bck_data_enc, "---\n") + 4; + $end_at = strpos($bck_data_enc, "\n---"); + $bck_data_enc = substr($bck_data_enc, $start_at, ($end_at - $start_at)); + } + $bck_data = $this->decrypt( + $bck_data_enc, + (string)$config->system->remotebackup->GDrivePassword + ); + if ($bck_data == $confdata) { + $target_filename = null; + } + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "unable to download " . + $configfiles[array_keys($configfiles)[0]]->description . " from Google Drive (" . $e . ")"); + } + } + if (!is_null($target_filename)) { + syslog(LOG_NOTICE, "backup configuration as " . $target_filename); + try { + $configfiles[$target_filename] = $client->upload( + (string)$config->system->remotebackup->GDriveFolderID, + $target_filename, + $confdata_enc + ); + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "unable to upload " . $target_filename . " to Google Drive (" . $e . ")"); + return array(); + } + + krsort($configfiles); + } + + // cleanup old files + if ( + isset($config->system->remotebackup->GDriveBackupCount) + && is_numeric((string)$config->system->remotebackup->GDriveBackupCount) + ) { + $fcount = 0; + foreach ($configfiles as $filename => $file) { + if ($fcount >= (string)$config->system->remotebackup->GDriveBackupCount) { + syslog(LOG_NOTICE, "remove " . $filename . " from Google Drive"); + try { + $client->delete($file); + } catch (Google_Service_Exception $e) { + syslog(LOG_ERR, "unable to remove " . $filename . " from Google Drive"); + } + } + $fcount++; + } + } + + // return filelist + return array_keys($configfiles); + } + } + + // not configured / issue, return empty list + return array(); + } + + /** + * Is this provider enabled + * @return boolean enabled status + */ + public function isEnabled() + { + $cnf = Config::getInstance(); + if ($cnf->isValid()) { + $config = $cnf->object(); + return isset($config->system->remotebackup) && isset($config->system->remotebackup->GDriveEnabled) + && !empty($config->system->remotebackup->GDriveEnabled); + } + return false; + } +} diff --git a/sysutils/git-backup/Makefile b/sysutils/git-backup/Makefile index 9c31c558f..747445597 100644 --- a/sysutils/git-backup/Makefile +++ b/sysutils/git-backup/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= git-backup -PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 3 +PLUGIN_VERSION= 1.1 +PLUGIN_REVISION= 1 PLUGIN_COMMENT= Track config changes using git PLUGIN_DEPENDS= git PLUGIN_MAINTAINER= ad@opnsense.org diff --git a/sysutils/git-backup/pkg-descr b/sysutils/git-backup/pkg-descr index 58fcb26dc..f8b99a6f0 100644 --- a/sysutils/git-backup/pkg-descr +++ b/sysutils/git-backup/pkg-descr @@ -1,3 +1,15 @@ This package adds a backup option using git version control. -Due to the sensitive nature of the data being send to the backup, we strongly advise to not use a public service to send backups to. +Due to the sensitive nature of the data being send to the backup, +we strongly advise to not use a public service to send backups to. + +Plugin Changelog +================ + +1.1 + +* Add a force-push option (contributed by Hleb Shauchenka) + +1.0 + +* Initial release diff --git a/sysutils/git-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Git.php b/sysutils/git-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Git.php index a109a1adc..f9573f33e 100644 --- a/sysutils/git-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Git.php +++ b/sysutils/git-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Git.php @@ -66,6 +66,13 @@ class Git extends Base implements IBackupProvider "help" => gettext("Target branch to push to."), "value" => null ], + [ + "name" => "force_push", + "type" => "checkbox", + "label" => gettext("Force Push"), + "help" => gettext("When enabled, force push to origin if diverged (e.g., after restoring from an earlier backup). Use with caution as this overwrites remote history."), + "value" => null + ], [ "name" => "privkey", "type" => "passwordarea", @@ -161,8 +168,9 @@ class Git extends Base implements IBackupProvider } exec("cd {$targetdir} && {$git} remote remove origin"); exec("cd {$targetdir} && {$git} remote add origin " . escapeshellarg($url)); + $force_flag = (string)$mdl->force_push === "1" ? "--force " : ""; $pushtxt = shell_exec( - "(cd {$targetdir} && {$git} push origin " . escapeshellarg("master:{$mdl->branch}") . + "(cd {$targetdir} && {$git} push {$force_flag}origin " . escapeshellarg("master:{$mdl->branch}") . " && echo '__exit_ok__') 2>&1" ); if (strpos($pushtxt, '__exit_ok__')) { diff --git a/sysutils/git-backup/src/opnsense/mvc/app/models/OPNsense/Backup/GitSettings.xml b/sysutils/git-backup/src/opnsense/mvc/app/models/OPNsense/Backup/GitSettings.xml index 7c1833770..dbd7b4ca8 100644 --- a/sysutils/git-backup/src/opnsense/mvc/app/models/OPNsense/Backup/GitSettings.xml +++ b/sysutils/git-backup/src/opnsense/mvc/app/models/OPNsense/Backup/GitSettings.xml @@ -1,23 +1,22 @@ //system/backup/git 1.0.0 - OPNsense Git Backup Settings + Git Backup Settings - 0 - Y - - - user.check001 - - - url.check001 - - + 0 + Y + + + user.check001 + + + url.check001 + + - N - /^((https)|(ssh))?:\/\/.*[^\/]$/ + /^((https)|(ssh))?:\/\/.*[^\/]$/ A valid git location must be provided. e.g. ssh://server/project.git, https://server/project.git @@ -30,22 +29,24 @@ - master - Y + master + Y - - N - + + 0 + Y + + - - - A username is required. - DependConstraint - - enabled - - - + + + A username is required. + DependConstraint + + enabled + + + diff --git a/sysutils/hw-probe/src/opnsense/mvc/app/models/OPNsense/Hwprobe/General.xml b/sysutils/hw-probe/src/opnsense/mvc/app/models/OPNsense/Hwprobe/General.xml index 96efb0749..b2ec998fb 100644 --- a/sysutils/hw-probe/src/opnsense/mvc/app/models/OPNsense/Hwprobe/General.xml +++ b/sysutils/hw-probe/src/opnsense/mvc/app/models/OPNsense/Hwprobe/General.xml @@ -4,7 +4,7 @@ 0.0.1 - 0 + 0 Y diff --git a/sysutils/mail-backup/src/opnsense/mvc/app/models/OPNsense/Backup/MailerSettings.xml b/sysutils/mail-backup/src/opnsense/mvc/app/models/OPNsense/Backup/MailerSettings.xml index 0a74af800..301849c9e 100644 --- a/sysutils/mail-backup/src/opnsense/mvc/app/models/OPNsense/Backup/MailerSettings.xml +++ b/sysutils/mail-backup/src/opnsense/mvc/app/models/OPNsense/Backup/MailerSettings.xml @@ -4,7 +4,7 @@ OPNsense Mailer Backup Settings - 0 + 0 Y @@ -30,11 +30,11 @@ - 25 + 25 Y - 1 + 1 Y diff --git a/sysutils/munin-node/src/opnsense/mvc/app/models/OPNsense/Muninnode/General.xml b/sysutils/munin-node/src/opnsense/mvc/app/models/OPNsense/Muninnode/General.xml index 64d22bdd6..85543a3e8 100644 --- a/sysutils/munin-node/src/opnsense/mvc/app/models/OPNsense/Muninnode/General.xml +++ b/sysutils/munin-node/src/opnsense/mvc/app/models/OPNsense/Muninnode/General.xml @@ -4,19 +4,19 @@ 0.0.2 - 0 + 0 Y - OPNsense + OPNsense Y - 4949 + 4949 Y - + N diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml b/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml index b8c63ab7f..026ec4ce5 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml @@ -4,12 +4,12 @@ OPNsense Nextcloud Backup Settings - 0 + 0 Y N - /^https?:\/\/.*[^\/]$/ + /^https?:\/\/.*[^\/]$/ The url must be valid without a trailing slash. For example: https://nextcloud.example.com or https://example.com/nextcloud @@ -48,8 +48,8 @@ Y - /^([\w%+\-]+\/)*[\w+%\-]+$/ - OPNsense-Backup + /^([\w%+\-]+\/)*[\w+%\-]+$/ + OPNsense-Backup The Backup Directory can only consist of alphanumeric characters, dash, underscores and slash. No leading or trailing slash. diff --git a/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.xml b/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.xml index 4f1d24539..a3af1f8e6 100644 --- a/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.xml +++ b/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.xml @@ -6,62 +6,62 @@ 0.2.0 - 0 + 0 Y - 0.0.0.0 + 0.0.0.0 Y N Please provide a valid IP address. - 9100 + 9100 Y Please provide a valid port number between 1 and 65535. Port 9100 is the default. - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 0 + 0 N - 0 + 0 N - 0 + 0 N - 0 + 0 N diff --git a/sysutils/nut/Makefile b/sysutils/nut/Makefile index 4d749383b..ed35203b9 100644 --- a/sysutils/nut/Makefile +++ b/sysutils/nut/Makefile @@ -1,5 +1,6 @@ PLUGIN_NAME= nut PLUGIN_VERSION= 1.9 +PLUGIN_REVISION= 1 PLUGIN_COMMENT= Network UPS Tools PLUGIN_DEPENDS= nut PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/sysutils/nut/pkg-descr b/sysutils/nut/pkg-descr index 15131a87a..4018fb54d 100644 --- a/sysutils/nut/pkg-descr +++ b/sysutils/nut/pkg-descr @@ -12,6 +12,7 @@ Plugin Changelog 1.9 * Add dashboard widget +* Fix IPv6 port declaration (contributed by BPplays) 1.8 diff --git a/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml b/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml index 3ee149b38..9de3788b5 100644 --- a/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml +++ b/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml @@ -5,11 +5,11 @@ - 0 + 0 Y - standalone + standalone Y standalone @@ -17,13 +17,13 @@ - UPSName + UPSName Y - /^([0-9a-zA-Z._\-]){1,128}$/u + /^([0-9a-zA-Z._\-]){1,128}$/u The name should only contain alphanumeric characters, dashes, underscores or a dot. - 127.0.0.1,::1 + 127.0.0.1,::1 Y @@ -31,42 +31,42 @@ Y - Password + Password Y - Password + Password Y - 0 + 0 - port=auto + port=auto N Y - 0 + 0 - port=auto + port=auto N Y - 0 + 0 Y - localhost + localhost N @@ -75,44 +75,44 @@ Y - 0 + 0 - port=auto + port=auto N Y - 0 + 0 - port=auto + port=auto N Y - 0 + 0 - port=auto + port=auto N Y - 0 + 0
        - + N
        - 3493 + 3493 N @@ -125,30 +125,30 @@ Y - 0 + 0 - port=auto + port=auto N Y - 0 + 0 - port=auto + port=auto N Y - 0 + 0 - community=public + community=public N diff --git a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf index da20af35d..e4d3ab405 100644 --- a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf +++ b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf @@ -7,7 +7,7 @@ SHUTDOWNCMD "/usr/local/etc/rc.halt" POWERDOWNFLAG /etc/killpower {% endif %} {% if helpers.exists('OPNsense.Nut.netclient.enable') and OPNsense.Nut.netclient.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }}@{{ OPNsense.Nut.netclient.address }}{% if helpers.exists('OPNsense.Nut.netclient.port') %}:{{ OPNsense.Nut.netclient.port }}{% endif %} 1 {{ OPNsense.Nut.netclient.user }} {{ OPNsense.Nut.netclient.password }} slave +MONITOR {{ OPNsense.Nut.general.name }}@{{ helpers.host_with_port('OPNsense.Nut.netclient.address', 'OPNsense.Nut.netclient.port') }} 1 {{ OPNsense.Nut.netclient.user }} {{ OPNsense.Nut.netclient.password }} slave SHUTDOWNCMD "/usr/local/etc/rc.halt" POWERDOWNFLAG /etc/killpower {% endif %} diff --git a/sysutils/puppet-agent/Makefile b/sysutils/puppet-agent/Makefile index a7112f55c..f79acafee 100644 --- a/sysutils/puppet-agent/Makefile +++ b/sysutils/puppet-agent/Makefile @@ -1,7 +1,7 @@ PLUGIN_NAME= puppet-agent -PLUGIN_VERSION= 1.1 +PLUGIN_VERSION= 1.2 PLUGIN_COMMENT= Manage Puppet Agent -PLUGIN_DEPENDS= puppet7 py${PLUGIN_PYTHON}-opn-cli +PLUGIN_DEPENDS= puppet8 py${PLUGIN_PYTHON}-opn-cli PLUGIN_MAINTAINER= jan.wink93@gmail.com .include "../../Mk/plugins.mk" diff --git a/sysutils/puppet-agent/pkg-descr b/sysutils/puppet-agent/pkg-descr index 3e8426bcb..0cd522fc5 100644 --- a/sysutils/puppet-agent/pkg-descr +++ b/sysutils/puppet-agent/pkg-descr @@ -9,6 +9,11 @@ WWW: https://puppet.com/docs/puppet/latest/man/agent.html Plugin Changelog ================ +1.2 + +Changed: +* switch to Puppet 8 (#4800) + 1.1 Added: diff --git a/sysutils/puppet-agent/src/opnsense/mvc/app/models/OPNsense/PuppetAgent/PuppetAgent.xml b/sysutils/puppet-agent/src/opnsense/mvc/app/models/OPNsense/PuppetAgent/PuppetAgent.xml index ffba1a25d..2e344467d 100644 --- a/sysutils/puppet-agent/src/opnsense/mvc/app/models/OPNsense/PuppetAgent/PuppetAgent.xml +++ b/sysutils/puppet-agent/src/opnsense/mvc/app/models/OPNsense/PuppetAgent/PuppetAgent.xml @@ -5,33 +5,33 @@ - 0 + 0 Y - puppet + puppet Y - production + production Y - /^.{1,100}$/u + /^.{1,100}$/u Should be a string between 1 and 100 characters. - 30m - /^([0-9]{1,8}(?:s|m|h|d|y)?)/u + 30m + /^([0-9]{1,8}(?:s|m|h|d|y)?)/u Should be a number between 1 and 8 characters, optionally followed by either "y", "d", "h", "m" or "s". Y - 1h - /^([0-9]{1,8}(?:s|m|h|d|y)?)/u + 1h + /^([0-9]{1,8}(?:s|m|h|d|y)?)/u Should be a number between 1 and 8 characters, optionally followed by either "y", "d", "h", "m" or "s". Y - 1 + 1 Y diff --git a/sysutils/puppet-agent/src/opnsense/mvc/app/views/OPNsense/PuppetAgent/index.volt b/sysutils/puppet-agent/src/opnsense/mvc/app/views/OPNsense/PuppetAgent/index.volt index 7e0d533c5..c9ca842c1 100644 --- a/sysutils/puppet-agent/src/opnsense/mvc/app/views/OPNsense/PuppetAgent/index.volt +++ b/sysutils/puppet-agent/src/opnsense/mvc/app/views/OPNsense/PuppetAgent/index.volt @@ -61,8 +61,8 @@ POSSIBILITY OF SUCH DAMAGE.

        {{ lang._("Welcome to the Puppet Agent plugin! This plugin allows you to integrate OPNsense with your Puppet environment.") }}

        {{ lang._("Keep in mind that you should not treat OPNsense like any other operating system. Most notably you should not modify system files or packages. Instead use the OPNsense API to make configuration changes and to manage plugins. The following tools are a good starting point when trying to automate OPNsense with Puppet:") }}

          -
        • {{ lang._("%sopn-cli:%s A command line client to configure OPNsense core and plugin components through their respective APIs.") | format('', '') }}
        • -
        • {{ lang._("%spuppet/opnsense:%s A read-to-use Puppet module for automating the OPNsense firewall.") | format('', '') }}
        • +
        • {{ lang._("%sopn-cli:%s A command line client to configure OPNsense core and plugin components through their respective APIs.") | format('', '') }}
        • +
        • {{ lang._("%spuppet/opnsense:%s A read-to-use Puppet module for automating the OPNsense firewall.") | format('', '') }}

        {{ lang._("Note that these tools are not directly related to this plugin. Please report issues and missing features directly to the author.") }}

        diff --git a/sysutils/sftp-backup/Makefile b/sysutils/sftp-backup/Makefile new file mode 100644 index 000000000..f5a698635 --- /dev/null +++ b/sysutils/sftp-backup/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= sftp-backup +PLUGIN_VERSION= 1.1 +PLUGIN_REVISION= 2 +PLUGIN_COMMENT= Backup configurations using SFTP +PLUGIN_MAINTAINER= ad@opnsense.org +PLUGIN_TIER= 2 + +.include "../../Mk/plugins.mk" diff --git a/sysutils/sftp-backup/pkg-descr b/sysutils/sftp-backup/pkg-descr new file mode 100644 index 000000000..372f032a0 --- /dev/null +++ b/sysutils/sftp-backup/pkg-descr @@ -0,0 +1,4 @@ +This plugin adds a backup option using SFTP (secure copy). + +Due to the sensitive nature of the data being send to the backup, +we strongly advise to not use a public service to send backups to. diff --git a/sysutils/sftp-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Sftp.php b/sysutils/sftp-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Sftp.php new file mode 100644 index 000000000..e817c00a7 --- /dev/null +++ b/sysutils/sftp-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Sftp.php @@ -0,0 +1,288 @@ +model = new SftpSettings(); + } + + /** + * @inheritdoc + */ + public function getConfigurationFields() + { + $fields = [ + [ + "name" => "enabled", + "type" => "checkbox", + "label" => gettext("Enable"), + "value" => null + ], + [ + "name" => "url", + "type" => "text", + "label" => gettext("URL"), + "help" => gettext( + "Target location, specified as uri, e.g. sftp://user@my.host.at.domain[:port]//path/to/backup" + ), + "value" => null + ], + [ + "name" => "privkey", + "type" => "passwordarea", + "label" => gettext("SSH private key"), + "help" => gettext("The private key used to setup the connection."), + "value" => null + ], + [ + "name" => "prefixhostname", + "type" => "checkbox", + "label" => gettext("Prefix hostname to backupfile"), + "help" => gettext("Normally the config xml will be written as config-stamp.xml, with this option set " . + "the filename will use the systems host and domain name."), + "value" => null + ], + [ + "name" => "backupcount", + "type" => "text", + "label" => gettext("Backup Count"), + "help" => gettext("Amount of backups to be kept at remote location. Set to 0 to upload latest only without housekeeping"), + "value" => 60 + ], + [ + "name" => "password", + "type" => "password", + "label" => gettext("Encrypt Password"), + "value" => null + ], + [ + "name" => "passwordconfirm", + "type" => "password", + "label" => gettext("Confirm"), + "value" => null + ] + ]; + foreach ($fields as &$field) { + if ($field['name'] == 'passwordconfirm') { + $field['value'] = (string)$this->model->getNodeByReference('password'); + } else { + $field['value'] = (string)$this->model->getNodeByReference($field['name']); + } + } + return $fields; + } + + /** + * @inheritdoc + */ + public function getName() + { + return gettext("sftp"); + } + + /** + * @inheritdoc + */ + public function setConfiguration($conf) + { + $this->setModelProperties($this->model, $conf); + $validation_messages = $this->validateModel($this->model); + if ($conf['passwordconfirm'] != $conf['password']) { + $validation_messages[] = gettext("The supplied 'Password' and 'Confirm' field values must match."); + } + if (empty($validation_messages)) { + $this->model->serializeToConfig(); + Config::getInstance()->save(); + } + return $validation_messages; + } + + /** + * sftp command + * @param string $sftpcmd command to execute + * @return array [stdout|stderr|exit_status] + */ + private function sftpCmd($sftpcmd) + { + $cmd = [ + '/usr/local/bin/sftp', + '-o StrictHostKeyChecking=accept-new', + '-o PasswordAuthentication=no', + '-o ChallengeResponseAuthentication=no', + '-i ' . $this->getIdentity(), + escapeshellarg($this->model->url) + ]; + + $result = ['exit_status' => -1, 'stderr' => '', 'stdout' => '']; + $process = proc_open( + implode(' ', $cmd), + [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]], + $pipes + ); + if (is_resource($process)) { + fwrite($pipes[0], $sftpcmd); + fclose($pipes[0]); + $result['stdout'] = stream_get_contents($pipes[1]); + fclose($pipes[1]); + $result['stderr'] = stream_get_contents($pipes[2]); + fclose($pipes[2]); + $result['exit_status'] = proc_close($process); + } + if ($result['exit_status'] !== 0) { + /* always throw on non zero exit status */ + syslog(LOG_ERR, "sftp-backup error (" . str_replace("\n", " ", $result['stderr']) . ")"); + throw new \Exception($result['stderr']); + } + return $result; + } + + /** + * @return identity file, create new when non existent + */ + private function getIdentity() + { + $confdir = "/conf/backup/sftp"; + $identfile = $confdir . '/identity'; + if (!is_dir($confdir)) { + mkdir($confdir); + } + $this_key = trim(str_replace("\r", "", $this->model->privkey)) . "\n"; + if (!is_file($identfile) || file_get_contents($identfile) != $this_key) { + File::file_put_contents($identfile, $this_key, 0600); + } + return $identfile; + } + + /** + * @return list of files on remote location + */ + private function ls($pattern = '') + { + $result = []; + foreach (explode("\n", $this->sftpCmd('ls -lnt ' . $pattern)['stdout']) as $line) { + $parts = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY); + if (count($parts) >= 7) { + $result[] = $parts[count($parts) - 1]; + } + } + return $result; + } + + /** + * @param string $source filename + * @param string $destination filename + */ + private function put($source, $destination) + { + $this->sftpCmd(sprintf('put %s %s', $source, $destination)); + } + + /** + * @param string $filename + */ + private function del($filename) + { + $this->sftpCmd(sprintf('rm %s', $filename)); + } + + /** + * @return array filelist + */ + public function backup() + { + $cnf = Config::getInstance(); + if (!$this->model->enabled->isEmpty() && $cnf->isValid()) { + if ($this->model->prefixhostname->isEmpty()) { + $fileprefix = "config-"; + } else { + $config = $cnf->object(); + $fileprefix = strtolower(sprintf('%s.%s-', (string)$config->system->hostname, (string)$config->system->domain)); + } + /** + * Collect most recent backup, since /conf/backup/ always contains the latests, we can use the filename + * for easy comparison. + **/ + $all_backups = glob('/conf/backup/config-*.xml'); + $most_recent = $all_backups[count($all_backups) - 1]; + $confdata = file_get_contents($most_recent); + if (!$this->model->password->isEmpty()) { + $confdata = $this->encrypt($confdata, (string)$this->model->password); + } + $remote_backups = $this->ls(sprintf('%s*.xml', $fileprefix)); + $target_filename = strtolower(preg_replace('/^config-/', $fileprefix, basename($most_recent))); + + if (!in_array($target_filename, $remote_backups)) { + syslog(LOG_NOTICE, "backup configuration as " . $target_filename); + $tmpfilename = sprintf("/conf/backup/sftp/%s", $target_filename); + File::file_put_contents($tmpfilename, $confdata, 0600); + $this->put($tmpfilename, $target_filename); + unlink($tmpfilename); + $remote_backups = $this->ls(sprintf('%s*.xml', $fileprefix)); + } + /* cleanup only if backup count is > 0*/ + if ($this->model->backupcount->asFloat() > 0) { + rsort($remote_backups); + if (count($remote_backups) > (int)$this->model->backupcount->getValue()) { + for ($i = $this->model->backupcount->getValue(); $i < count($remote_backups); $i++) { + $this->del($remote_backups[$i]); + } + $remote_backups = $this->ls(sprintf('%s*.xml', $fileprefix)); + } + return $remote_backups; + } else { + return $this->ls(sprintf('%s*.xml', $fileprefix)) ?: []; + } + } else { + /* disabled */ + return; + } + } + + /** + * @inheritdoc + */ + public function isEnabled() + { + return !$this->model->enabled->isEmpty(); + } +} diff --git a/sysutils/sftp-backup/src/opnsense/mvc/app/models/OPNsense/Backup/SftpSettings.php b/sysutils/sftp-backup/src/opnsense/mvc/app/models/OPNsense/Backup/SftpSettings.php new file mode 100644 index 000000000..4cad4acf7 --- /dev/null +++ b/sysutils/sftp-backup/src/opnsense/mvc/app/models/OPNsense/Backup/SftpSettings.php @@ -0,0 +1,39 @@ + + //system/backup/sftp + 1.0.0 + OPNsense sftp Backup Settings + + + 0 + Y + + + privkey.check001 + + + url.check001 + + + + + N + /^((sftp))?:\/\/.*[^\/]$/ + A valid location must be provided. + + + A backup location (url) is required. + DependConstraint + + enabled + + + + + + N + + + A private key is required. + DependConstraint + + enabled + + + + + + + + 60 + Y + 0 + + + 0 + N + + +
        diff --git a/sysutils/smart/Makefile b/sysutils/smart/Makefile index 2afa17e45..132bbe4b3 100644 --- a/sysutils/smart/Makefile +++ b/sysutils/smart/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= smart -PLUGIN_VERSION= 2.3 +PLUGIN_VERSION= 2.4 PLUGIN_COMMENT= SMART tools PLUGIN_DEPENDS= smartmontools PLUGIN_MAINTAINER= franco@opnsense.org diff --git a/sysutils/smart/src/opnsense/mvc/app/controllers/OPNsense/Smart/Api/ServiceController.php b/sysutils/smart/src/opnsense/mvc/app/controllers/OPNsense/Smart/Api/ServiceController.php index d35feb29b..2f039c551 100644 --- a/sysutils/smart/src/opnsense/mvc/app/controllers/OPNsense/Smart/Api/ServiceController.php +++ b/sysutils/smart/src/opnsense/mvc/app/controllers/OPNsense/Smart/Api/ServiceController.php @@ -68,7 +68,7 @@ class ServiceController extends ApiControllerBase return array("message" => "Invalid device name"); } - $valid_info_types = array("i", "H", "c", "A", "a"); + $valid_info_types = array("i", "H", "c", "A", "a", "x"); if (!in_array($type, $valid_info_types)) { return array("message" => "Invalid info type"); diff --git a/sysutils/smart/src/opnsense/mvc/app/views/OPNsense/Smart/index.volt b/sysutils/smart/src/opnsense/mvc/app/views/OPNsense/Smart/index.volt index acb740ffa..1e770f836 100644 --- a/sysutils/smart/src/opnsense/mvc/app/views/OPNsense/Smart/index.volt +++ b/sysutils/smart/src/opnsense/mvc/app/views/OPNsense/Smart/index.volt @@ -138,7 +138,8 @@       - +   + diff --git a/sysutils/smart/src/opnsense/service/conf/actions.d/actions_smart.conf b/sysutils/smart/src/opnsense/service/conf/actions.d/actions_smart.conf index 03a594580..b24bc6d56 100644 --- a/sysutils/smart/src/opnsense/service/conf/actions.d/actions_smart.conf +++ b/sysutils/smart/src/opnsense/service/conf/actions.d/actions_smart.conf @@ -12,59 +12,68 @@ message:list installed devices [info] command:/usr/local/sbin/smartctl -parameters:-%s %s; exit 0 +parameters:-%s %s +errors:no type:script_output message:exec smartctl -%s for device %s [info_json] command:/usr/local/sbin/smartctl -parameters:-%s --json=c %s; exit 0 +parameters:-%s --json=c %s +errors:no type:script_output message:exec smartctl -%s (JSON) for device %s [log.error] command:/usr/local/sbin/smartctl -l error -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Get error log for device %s [log.selftest] command:/usr/local/sbin/smartctl -l selftest -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Get selftest log for device %s [test.offline] command:/usr/local/sbin/smartctl -t offline -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Testing device %s (offline) description:Run SMART test (offline) [test.short] command:/usr/local/sbin/smartctl -t short -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Testing device %s (short) description:Run SMART test (short) [test.long] command:/usr/local/sbin/smartctl -t long -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Testing device %s (long) description:Run SMART test (long) [test.conveyance] command:/usr/local/sbin/smartctl -t conveyance -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Testing device %s (conveyance) description:Run SMART test (conveyance) [abort] command:/usr/local/sbin/smartctl -X -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Abort test on device %s description:Abort SMART tests diff --git a/vendor/sunnyvalley/Makefile b/vendor/sunnyvalley/Makefile index 4a44d5a87..1dcd36551 100644 --- a/vendor/sunnyvalley/Makefile +++ b/vendor/sunnyvalley/Makefile @@ -1,8 +1,9 @@ PLUGIN_NAME= sunnyvalley -PLUGIN_VERSION= 1.4 -PLUGIN_REVISION= 3 -PLUGIN_COMMENT= Vendor Repository for Zenarmor (a.k.a Sensei, Next Generation Firewall Extensions) -PLUGIN_MAINTAINER= opensource@sunnyvalley.io +PLUGIN_VERSION= 1.5 +PLUGIN_REVISION= 1 +PLUGIN_COMMENT= Vendor Repository for Zenarmor (Enterprise Security Modules - NGFW, SSE, SASE, f.k.a Sensei) +PLUGIN_MAINTAINER= opensource@zenarmor.com PLUGIN_WWW= https://www.zenarmor.com +PLUGIN_TIER= 2 .include "../../Mk/plugins.mk" diff --git a/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.shadow.in b/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.shadow.in index 4bc181315..8aced05ba 100644 --- a/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.shadow.in +++ b/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.shadow.in @@ -1,6 +1,6 @@ SunnyValley: { fingerprints: "/usr/local/etc/pkg/fingerprints/SunnyValley", - url: "https://updates.zenarmor.com/opnsense/${ABI}/%%PLUGIN_ABI%%/latest", + url: "https://updates.zenarmor.net/opnsense/${ABI}/%%PLUGIN_ABI%%/latest", signature_type: "fingerprints", priority: 7, enabled: yes diff --git a/www/OPNProxy/Makefile b/www/OPNProxy/Makefile index 3c1b5d999..c225b41a1 100644 --- a/www/OPNProxy/Makefile +++ b/www/OPNProxy/Makefile @@ -1,11 +1,10 @@ PLUGIN_NAME= OPNProxy PLUGIN_VERSION= 1.0.5 -PLUGIN_REVISION= 1 +PLUGIN_REVISION= 3 PLUGIN_COMMENT= OPNsense proxy additions PLUGIN_DEPENDS= os-redis${PLUGIN_PKGSUFFIX} \ os-squid${PLUGIN_PKGSUFFIX} \ py${PLUGIN_PYTHON}-redis PLUGIN_MAINTAINER= ad@opnsense.org -PLUGIN_TIER= 2 .include "../../Mk/plugins.mk" diff --git a/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/ACL.xml b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/ACL.xml index 345959ae3..4347f95f3 100644 --- a/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/ACL.xml +++ b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/ACL.xml @@ -84,11 +84,11 @@ N N , - Y + Y Y - /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){1,255}$/u + /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){1,255}$/u Description should be a string between 1 and 255 characters @@ -119,11 +119,11 @@ N N , - Y + Y Y - /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){1,255}$/u + /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){1,255}$/u Description should be a string between 1 and 255 characters diff --git a/www/OPNProxy/src/opnsense/mvc/app/views/Deciso/Proxy/acl.volt b/www/OPNProxy/src/opnsense/mvc/app/views/Deciso/Proxy/acl.volt index 9d25467fd..1abc4ff78 100644 --- a/www/OPNProxy/src/opnsense/mvc/app/views/Deciso/Proxy/acl.volt +++ b/www/OPNProxy/src/opnsense/mvc/app/views/Deciso/Proxy/acl.volt @@ -4,22 +4,22 @@ $("#apply_div").show(); if (e.target.id == 'policies_tab') { $("#grid-policies").UIBootgrid( - { search:'/api/proxy/acl/searchPolicy/', - get:'/api/proxy/acl/getPolicy/', - set:'/api/proxy/acl/setPolicy/', - add:'/api/proxy/acl/addPolicy/', - del:'/api/proxy/acl/delPolicy/', - toggle:'/api/proxy/acl/togglePolicy/' + { search:'/api/proxy/acl/search_policy/', + get:'/api/proxy/acl/get_policy/', + set:'/api/proxy/acl/set_policy/', + add:'/api/proxy/acl/add_policy/', + del:'/api/proxy/acl/del_policy/', + toggle:'/api/proxy/acl/toggle_policy/' } ); } else if (e.target.id == 'custom_policies_tab') { $("#grid-custom_policies").UIBootgrid( - { search:'/api/proxy/acl/searchCustomPolicy/', - get:'/api/proxy/acl/getCustomPolicy/', - set:'/api/proxy/acl/setCustomPolicy/', - add:'/api/proxy/acl/addCustomPolicy/', - del:'/api/proxy/acl/delCustomPolicy/', - toggle:'/api/proxy/acl/toggleCustomPolicy/' + { search:'/api/proxy/acl/search_custom_policy/', + get:'/api/proxy/acl/get_custom_policy/', + set:'/api/proxy/acl/set_custom_policy/', + add:'/api/proxy/acl/add_custom_policy/', + del:'/api/proxy/acl/del_custom_policy/', + toggle:'/api/proxy/acl/toggle_custom_policy/' } ); } else if (e.target.id == 'policy_tester_tab') { diff --git a/www/OPNProxy/src/opnsense/scripts/OPNProxy/redis_sync_users.py b/www/OPNProxy/src/opnsense/scripts/OPNProxy/redis_sync_users.py index 3e22e0909..30098333e 100755 --- a/www/OPNProxy/src/opnsense/scripts/OPNProxy/redis_sync_users.py +++ b/www/OPNProxy/src/opnsense/scripts/OPNProxy/redis_sync_users.py @@ -60,9 +60,12 @@ if __name__ == '__main__': membership = dict() for group in xmlroot.findall('./system/group'): for member in group.findall('member'): - if member.text not in membership: - membership[member.text] = list() - membership[member.text].append(group.findtext('name')) + if member.text is None: + continue + for item in member.text.split(','): + if item not in membership: + membership[item] = list() + membership[item].append(group.findtext('name')) for user in xmlroot.findall('./system/user'): if args.username is None or args.username == user.findtext('name'): diff --git a/www/c-icap/Makefile b/www/c-icap/Makefile index 90a88fdf1..c18f9d74b 100644 --- a/www/c-icap/Makefile +++ b/www/c-icap/Makefile @@ -1,8 +1,7 @@ PLUGIN_NAME= c-icap -PLUGIN_VERSION= 1.7 -PLUGIN_REVISION= 5 +PLUGIN_VERSION= 1.9 PLUGIN_COMMENT= c-icap connects the web proxy with a virus scanner PLUGIN_DEPENDS= c-icap c-icap-modules -PLUGIN_MAINTAINER= m.muenz@gmail.com +PLUGIN_MAINTAINER= andybinder@gmx.de .include "../../Mk/plugins.mk" diff --git a/www/c-icap/pkg-descr b/www/c-icap/pkg-descr index 5ae299e86..2369c75a1 100644 --- a/www/c-icap/pkg-descr +++ b/www/c-icap/pkg-descr @@ -2,4 +2,14 @@ c-icap is an implementation of an ICAP server. It can be used with HTTP proxies that support the ICAP protocol to implement content adaptation and filtering services. -WWW: http://c-icap.sourceforge.net/ +Plugin Changelog +================ + +1.9 + +* Fix issue with localserver ACL defintion +* Yield maintainership to Andy Binder + +1.8 + +* Move logging to syslog (contributed by Andy Binder) diff --git a/www/c-icap/src/etc/inc/plugins.inc.d/cicap.inc b/www/c-icap/src/etc/inc/plugins.inc.d/cicap.inc index 305408d15..d2528499d 100644 --- a/www/c-icap/src/etc/inc/plugins.inc.d/cicap.inc +++ b/www/c-icap/src/etc/inc/plugins.inc.d/cicap.inc @@ -26,6 +26,15 @@ POSSIBILITY OF SUCH DAMAGE. */ +function cicap_syslog() +{ + return [ + 'cicap' => [ + 'facility' => ['c-icap'] + ] + ]; +} + function cicap_services() { global $config; diff --git a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Antivirus.xml b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Antivirus.xml index a0310b9df..d31805eb8 100644 --- a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Antivirus.xml +++ b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Antivirus.xml @@ -4,12 +4,12 @@ 1.0.0 - 0 + 0 Y - TEXT,DATA,EXECUTABLE,ARCHIVE,GIF,JPEG,MSOFFICE - Y + TEXT,DATA,EXECUTABLE,ARCHIVE,GIF,JPEG,MSOFFICE + Y Y Text files @@ -22,23 +22,23 @@ - 5 + 5 Y - 2M + 2M Y - 1 + 1 Y - 0 + 0 Y - 5M + 5M Y diff --git a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/General.xml b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/General.xml index 7060936c0..8f1d7232a 100644 --- a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/General.xml +++ b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/General.xml @@ -4,66 +4,66 @@ 1.0.1 - 0 + 0 Y - 300 + 300 Y - 100 + 100 Y - 600 + 600 Y - 3 + 3 Y - 10 + 10 Y - 10 + 10 Y - 20 + 20 Y - 10 + 10 Y - 0 + 0 Y - ::1 + ::1 N N , Y - + N - + N - 1 + 1 Y - 1 + 1 Y diff --git a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Menu/Menu.xml b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Menu/Menu.xml index 326a689d0..f408f8109 100644 --- a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Menu/Menu.xml +++ b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Menu/Menu.xml @@ -2,7 +2,7 @@ - + diff --git a/www/c-icap/src/opnsense/scripts/OPNsense/CICAP/setup.sh b/www/c-icap/src/opnsense/scripts/OPNsense/CICAP/setup.sh index 8e29386a1..b5b219563 100755 --- a/www/c-icap/src/opnsense/scripts/OPNsense/CICAP/setup.sh +++ b/www/c-icap/src/opnsense/scripts/OPNsense/CICAP/setup.sh @@ -4,11 +4,5 @@ mkdir -p /var/run/c-icap chown -R c_icap:c_icap /var/run/c-icap chmod 750 /var/run/c-icap -mkdir -p /var/log/c-icap -chown -R c_icap:c_icap /var/log/c-icap -chmod 750 /var/log/c-icap -(cd /var/log && ln -s c-icap cicap) -chown -R c_icap:c_icap /var/log/cicap - mkdir -p /tmp/c-icap/templates/virus_scan/en chmod -R 755 /tmp/c-icap/ diff --git a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/+TARGETS b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/+TARGETS index 4be92f826..07687be79 100644 --- a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/+TARGETS +++ b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/+TARGETS @@ -1,6 +1,5 @@ c_icap:/etc/rc.conf.d/c_icap c-icap.conf:/usr/local/etc/c-icap/c-icap.conf -newsyslog.conf:/etc/newsyslog.conf.d/c-icap virus_scan.conf:/usr/local/etc/c-icap/virus_scan.conf VIRUS_FOUND:/tmp/c-icap/templates/virus_scan/en/VIRUS_FOUND VIR_MODE_HEAD:/tmp/c-icap/templates/virus_scan/en/VIR_MODE_HEAD diff --git a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c-icap.conf b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c-icap.conf index 27b16b44e..f2ea03556 100644 --- a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c-icap.conf +++ b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c-icap.conf @@ -42,11 +42,15 @@ ServerName {{ OPNsense.cicap.general.servername }} {% else %} ServerName {{ system.hostname }} {% endif %} +{% if not helpers.empty('OPNsense.cicap.general.listenaddress') %} +acl localserver srvip {{ OPNsense.cicap.general.listenaddress }} +{% else %} +acl localserver srvip ::1 +{% endif %} {% if helpers.exists('OPNsense.cicap.general.localSquid') and OPNsense.cicap.general.localSquid == '1' %} {% if helpers.exists('OPNsense.proxy.forward.icap.SendUsername') and OPNsense.proxy.forward.icap.SendUsername == '1' %} RemoteProxyUsers on acl AUTH auth * -acl localserver srvip 127.0.0.1 icap_access allow AUTH localserver {% else %} RemoteProxyUsers off @@ -62,7 +66,6 @@ RemoteProxyUserHeader {{OPNsense.proxy.forward.icap.UsernameHeader}} {% else %} RemoteProxyUsers on acl AUTH auth * -acl localserver srvip 127.0.0.1 icap_access allow AUTH localserver RemoteProxyUserHeaderEncoded on RemoteProxyUserHeader X-Authenticated-User @@ -77,9 +80,11 @@ ServicesDir /usr/local/lib/c_icap TemplateDir /tmp/c-icap/templates/ TemplateDefaultLanguage en LoadMagicFile /usr/local/etc/c-icap/c-icap.magic -ServerLog /var/log/c-icap/server.log -{% if helpers.exists('OPNsense.cicap.general.enable_accesslog') and OPNsense.cicap.general.enable_accesslog == '1' %} -AccessLog /var/log/c-icap/access.log +Module logger sys_logger.so +Logger sys_logger +sys_logger.Prefix "c-icap" +{% if helpers.exists('OPNsense.cicap.general.enable_accesslog') and OPNsense.cicap.general.enable_accesslog == '0' %} +sys_logger.access !localserver {% endif %} Service echo srv_echo.so Include virus_scan.conf diff --git a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/newsyslog.conf b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/newsyslog.conf deleted file mode 100644 index 1edd4401f..000000000 --- a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/newsyslog.conf +++ /dev/null @@ -1,7 +0,0 @@ -# logfilename [owner:group] mode count size when flags [/pid_file] [sig_num] -{% if helpers.exists('OPNsense.cicap.general.enabled') and OPNsense.cicap.general.enabled|default("0") == "1" %} -{% if helpers.exists('OPNsense.cicap.general.enable_accesslog') and OPNsense.cicap.general.enable_accesslog == '1' %} -/var/log/c-icap/access.log c_icap:c_icap 644 7 * @T00 ZB /var/run/c-icap/c-icap.pid -{% endif %} -/var/log/c-icap/server.log c_icap:c_icap 644 7 * @T00 ZB /var/run/c-icap/c-icap.pid -{% endif %} diff --git a/www/c-icap/src/opnsense/service/templates/OPNsense/Syslog/local/cicap.conf b/www/c-icap/src/opnsense/service/templates/OPNsense/Syslog/local/cicap.conf new file mode 100644 index 000000000..9eee81b26 --- /dev/null +++ b/www/c-icap/src/opnsense/service/templates/OPNsense/Syslog/local/cicap.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [cicap]. +################################################################### +filter f_local_cicap { + program("c-icap"); +}; diff --git a/www/caddy/Makefile b/www/caddy/Makefile index 0a03f1284..120806956 100644 --- a/www/caddy/Makefile +++ b/www/caddy/Makefile @@ -1,5 +1,6 @@ PLUGIN_NAME= caddy -PLUGIN_VERSION= 1.8.1 +PLUGIN_VERSION= 2.0.4 +PLUGIN_REVISION= 1 PLUGIN_DEPENDS= caddy-custom PLUGIN_COMMENT= Modern Reverse Proxy with Automatic HTTPS, Dynamic DNS and Layer4 Routing PLUGIN_MAINTAINER= cedrik@pischem.com diff --git a/www/caddy/pkg-descr b/www/caddy/pkg-descr index c9583ba43..c4a06b79d 100644 --- a/www/caddy/pkg-descr +++ b/www/caddy/pkg-descr @@ -1,11 +1,4 @@ -Caddy - The Ultimate Server - makes your sites more secure, more reliable, and more scalable than any other solution. -By default, Caddy automatically obtains and renews TLS certificates (Let's Encrypt and ZeroSSL) for all your sites. -It's the most advanced HTTPS server in the world. - -* Reverse Proxy HTTP, HTTPS and WebSockets -* Route UDP/TCP traffic with the included Layer4 module: https://github.com/mholt/caddy-l4 -* Dynamic DNS module included: https://github.com/mholt/caddy-dynamicdns -* Large selection of DNS Providers available: https://github.com/caddy-dns +Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS WWW: https://caddyserver.com/ DOC: https://docs.opnsense.org/manual/how-tos/caddy.html @@ -13,6 +6,83 @@ DOC: https://docs.opnsense.org/manual/how-tos/caddy.html Plugin Changelog ================ +2.0.4 + +Add: DNS-01 challenge delegation via CNAME (contributed by sdsys-ch) (opnsense/plugins/pull/4950) +Fix: Enabling HTTP access log wrongly excluded the process logs (opnsense/plugins/pull/4974) +Fix: fix setup.sh script not setting correct ownership in www user mode (opnsense/plugins/pull/4976) + +2.0.3 + +Add: Tabulator groupBy of domain and subdomain (opnsense/plugins/pull/4909) +Cleanup: Grid HTML and style +Fix: Tabulator 'Data Load Response Blocked' warning +Fix: setup.sh interaction with caddy storage and permissions (opnsense/plugins/pull/4911) +Fix: Emit subdomain http access logs when wildcard parent has logging enabled (opnsense/plugins/issues/4914) + +2.0.2 + +Add: Global server timeout options (opnsense/plugins/pull/4778) +Cleanup: Change all camelCase to snake_case in api notations (opnsense/plugins/pull/4767,4768,4776) +Cleanup: Use SimpleActionButton in general.volt (opnsense/plugins/pull/4779) +Cleanup: Change em into px, fix key/value display in formatter (opnsense/plugins/pull/4807) +Cleanup: Remove obsolete model_relation_domain formatter (opnsense/plugins/pull/4813) + +2.0.1 + +Add: Active health checks to handlers (contributed by zaben903) (opnsense/plugins/pull/4721) +Fix: Add handler command sometimes clearing domain selection in open dialogue (opnsense/plugins/pull/4748) + +2.0.0 + +* Attention - Breaking Change: All DNS Providers except Cloudflare have been removed and will not come back (opnsense/plugins/pull/4691) + Going forward, if DNS-01 challenge or Dynamic DNS is a requirement, you could use os-acme-client and os-ddclient. + Another option is to migrate to Cloudflare, as this module will stay a core component. + +* Add: Request Matcher option to Access List (contributed by spawntty) (opnsense/plugins/pull/4680) +* Add: ECH can configure encrypted client hello with Cloudflare (opnsense/plugins/pull/4673) +* Add: Client Auth (mTLS) to subdomains (opnsense/plugins/pull/4673) +* Build: Update to caddy-v2.10.0 (opnsense/tools/pull/469) +* Change: Render subdomains in a new pattern in the Caddyfile to adhere to caddy-v2.10.0 standard (opnsense/plugins/pull/4673) +* Change: Show only wildcard domains in subdomain selection (opnsense/plugins/pull/4646) +* Fix: Missing translation and toggle filter icon (opnsense/plugins/pull/4648) + +1.8.5 + +* Add: basic_auth per handler (opnsense/plugins/issues/4619) +* Change: the ACL has been changed to "page-caddy" in "System: Access: Privileges". Privilege must be reassigned if used. (opnsense/plugins/issues/4623) +* Change: standalone certificate widget has been removed, use system default certificate widget instead. (opnsense/plugins/pull/4637) +* Cleanup: UI improvements (opnsense/plugins/pull/4634) + * Cleanup of styles, formatters, and dialog layouts + * Dialog element hiding logic was improved + * "Filter by Domain" can filter subdomains + * "Filter by Domain" will preselect domain and subdomain in handler add dialog + * Grids are now responsive with automatic overflow on small screens + * Grids now lazy load on tab switch for improved performance + * Grids format domains and upstreams with protocol, domain, port and path in a single line + * "Layer4 Proxy" and "Reverse Proxy" now use the same volt template + * "Search Handler" and "Add Handler" buttons added to domain and subdomain grids + * Step buttons (Add Domain, Add Handler) have been removed since they are redundant + * Success messages on configuration apply have been removed, only errors will be shown + * Tabs use URL hash to preserve selection on refresh + +1.8.4 + +* Add: Client Auth (mTLS) to domains (opnsense/plugins/issues/4089) +* Fix: Some selectpickers had incorrect style (opnsense/plugins/pull/4616) + +1.8.3 + +* Add: Update DNS Providers with new optional choices (opnsense/plugins/issues/4543) +* Add: propagation_timeout and propagation_delay (opnsense/plugins/issues/4544) + +1.8.2 + +* Add: client_ip_headers (opnsense/plugins/issues/4517) +* Add: CloudDNS provider (opnsense/plugins/pull/4507) +* Change: Generalize forward_auth copy_headers directive. Existing configuration from (issues/4488) will be emptied. (opnsense/plugins/pull/4519) +* Fix: Shortcut buttons in reverse_proxy.volt (opnsense/plugins/pull/4525) + 1.8.1 * Add: Optional "Authorization" header to forward_auth (opnsense/plugins/issues/4488) diff --git a/www/caddy/src/etc/ssl/ext_sources/caddy.conf b/www/caddy/src/etc/ssl/ext_sources/caddy.conf new file mode 100644 index 000000000..08b1368b5 --- /dev/null +++ b/www/caddy/src/etc/ssl/ext_sources/caddy.conf @@ -0,0 +1,4 @@ +[location] +base=/var/db/caddy/data/caddy/certificates +pattern=.*\.crt$ +description=Caddy diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/DiagnosticsController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/DiagnosticsController.php index edcb55389..f8de1f2c1 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/DiagnosticsController.php +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/DiagnosticsController.php @@ -81,23 +81,4 @@ class DiagnosticsController extends ApiMutableModelControllerBase // Return the response as an array which gets automatically encoded to JSON return ["status" => "success", "content" => $responseArray['content']]; } - - /** - * Fetch the hostnames, validity and expiration dates of automatic certificates as JSON. Consumed by Caddy widget. - */ - public function certificateAction() - { - $backend = new Backend(); - $response = $backend->configdRun('caddy certificate'); - - // Decode JSON to PHP array - $responseArray = json_decode($response, true); - - if (isset($responseArray['error'])) { - return ["status" => "failed", "message" => $responseArray['message']]; - } - - // Return the response as an array which gets automatically encoded to JSON - return ["status" => "success", "content" => $responseArray]; - } } diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ReverseProxyController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ReverseProxyController.php index 742fb4e17..70a63b0d2 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ReverseProxyController.php +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ReverseProxyController.php @@ -38,77 +38,93 @@ class ReverseProxyController extends ApiMutableModelControllerBase protected static $internalModelUseSafeDelete = true; /** - * Function for search filter dropdown - * - * @return array containing rows of domain and port combinations. + * Return selectpicker options for reverse proxy domains and ports */ public function getAllReverseDomainsAction() { - $result = array("rows" => array()); + $result = [ + 'domains' => [ + 'label' => gettext('Domains'), + 'icon' => 'fa fa-fw fa-globe text-success', + 'items' => [] + ], + 'subdomains' => [ + 'label' => gettext('Subdomains'), + 'icon' => 'fa fa-fw fa-globe text-warning', + 'items' => [] + ], + ]; - $mdlCaddy = new \OPNsense\Caddy\Caddy(); - $reverseNodes = $mdlCaddy->reverseproxy->reverse->iterateItems(); + foreach ((new \OPNsense\Caddy\Caddy())->reverseproxy->reverse->iterateItems() as $reverse) { + if (!empty($reverse->FromDomain)) { + $port = (string)$reverse->FromPort; + $combined = (string)$reverse->FromDomain . ($port !== '' ? ':' . $port : ''); - foreach ($reverseNodes as $item) { - if (!empty($item->FromDomain)) { - // Conditionally concatenate port if it exists - $domain = (string)$item->FromDomain; - $port = (string)$item->FromPort; - $combinedDomainPort = $domain . (!empty($port) ? ':' . $port : ''); - - $result['rows'][] = array( - 'id' => (string)$item->getAttributes()['uuid'], - 'domainPort' => $combinedDomainPort // Combined domain and port, conditionally adding port - ); + $result['domains']['items'][] = [ + 'value' => (string)$reverse->getAttributes()['uuid'], + 'label' => $combined + ]; } } + foreach ((new \OPNsense\Caddy\Caddy())->reverseproxy->subdomain->iterateItems() as $subdomain) { + if (!empty($subdomain->FromDomain)) { + $result['subdomains']['items'][] = [ + 'value' => (string)$subdomain->getAttributes()['uuid'], + 'label' => (string)$subdomain->FromDomain + ]; + } + } + + foreach (array_keys($result) as $key) { + usort($result[$key]['items'], fn($a, $b) => strcasecmp($a['label'], $b['label'])); + } + return $result; } /** - * Generalized helper function for searching across different sections of the reverse proxy setup. - * This function mostly helps when model relation fields are used. - * It filters entries based on UUIDs provided as an argument. The section or key used for the UUID - * can be specified, allowing for direct or indirect UUID referencing. + * Build a UUID filter function. * - * @param string $modelPath The data model path identifier, pointing to section of model being searched. - * @param string $uuidSearchBase The request parameter name for the comma-separated list of UUIDs. - * @param string|null $uuidReferenceKey Attribute key used to fetch the UUID for filtering. - * If null, uses item's own UUID. - * @return array Filtered search results. + * @return callable|null */ - private function searchActionHelper($modelPath, $uuidSearchBase, $uuidReferenceKey = null) + private function buildFilterFunction(): ?callable { - // Fetch the comma-separated UUIDs string from the request using the provided parameter name. - $uuidList = $this->request->get($uuidSearchBase); - // Ensure the retrieved UUID list is a string and not empty before attempting to explode it. - $uuidArray = (!empty($uuidList) && is_string($uuidList)) ? explode(',', $uuidList) : []; + $domainUuids = $this->request->get('domainUuids'); + if (empty($domainUuids)) { + return null; + } - // Define a filter function to determine which items to include based on the UUID. - $filterFunction = function ($modelItem) use ($uuidArray, $uuidReferenceKey) { - // Extract UUID from the item, using the specified UUID key if provided, else default to direct UUID access. - if ($uuidReferenceKey !== null) { - $modelUUID = (string)$modelItem->$uuidReferenceKey; - } else { - $modelUUID = (string)$modelItem->getAttributes()['uuid']; + return function ($record) use ($domainUuids): bool { + $fieldsToCheck = ['reverse', 'subdomain']; + $uuids = []; + + // Add the record's own UUID + $uuidAttr = $record->getAttributes()['uuid'] ?? null; + if (is_scalar($uuidAttr)) { + $uuids[] = trim((string)$uuidAttr); } - // Include the item if the UUID array is empty or if the item's UUID is in the array. - return empty($uuidArray) || in_array($modelUUID, $uuidArray, true); + + // Add UUIDs from model relation fields + foreach ($fieldsToCheck as $field) { + $value = $record->{$field} ?? null; + + foreach ((array)$value as $item) { + if (is_scalar($item)) { + $uuids[] = trim((string)$item); + } + } + } + + return (bool) array_intersect($uuids, $domainUuids); }; - - // Perform the search using the specified model path and the filter function, returning the results. - // Note: This uses the existing search function of the ApiMutableModelControllerBase - return $this->searchBase($modelPath, null, 'description', $filterFunction); } - // ReverseProxy Section public function searchReverseProxyAction() { - // For domains, use their domain UUIDs directly, $uuidReferenceKey null added for explicit clarity - return $this->searchActionHelper("reverseproxy.reverse", "reverseUuids", null); + return $this->searchBase('reverseproxy.reverse', null, null, $this->buildFilterFunction()); } public function setReverseProxyAction($uuid) @@ -141,9 +157,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase public function searchSubdomainAction() { - // For subdomains, compare 'reverseUuids' (which contain domain UUIDs) - // to 'reverse' (which contain the same domain UUIDs due to model relation field) - return $this->searchActionHelper("reverseproxy.subdomain", "reverseUuids", "reverse"); + return $this->searchBase('reverseproxy.subdomain', null, null, $this->buildFilterFunction()); } public function setSubdomainAction($uuid) @@ -174,12 +188,9 @@ class ReverseProxyController extends ApiMutableModelControllerBase // Handler Section - // Adjusted for search filter dropdown, using helper function public function searchHandleAction() { - // For handles, compare 'reverseUuids' (which contain domain UUIDs) - // to 'reverse' (which contain the same domain UUIDs due to model relation field) - return $this->searchActionHelper("reverseproxy.handle", "reverseUuids", "reverse"); + return $this->searchBase('reverseproxy.handle', null, null, $this->buildFilterFunction()); } public function setHandleAction($uuid) @@ -211,7 +222,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase public function searchAccessListAction() { - return $this->searchBase("reverseproxy.accesslist", null, 'description'); + return $this->searchBase("reverseproxy.accesslist"); } public function setAccessListAction($uuid) @@ -239,7 +250,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase public function searchBasicAuthAction() { - return $this->searchBase("reverseproxy.basicauth", null, 'description'); + return $this->searchBase("reverseproxy.basicauth"); } public function setBasicAuthAction($uuid) @@ -291,7 +302,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase public function searchHeaderAction() { - return $this->searchBase("reverseproxy.header", null, 'description'); + return $this->searchBase("reverseproxy.header"); } public function setHeaderAction($uuid) @@ -319,7 +330,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase public function searchLayer4Action() { - return $this->searchBase("reverseproxy.layer4", null, 'description'); + return $this->searchBase("reverseproxy.layer4"); } public function setLayer4Action($uuid) @@ -352,7 +363,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase public function searchLayer4OpenvpnAction() { - return $this->searchBase("reverseproxy.layer4openvpn", null, 'description'); + return $this->searchBase("reverseproxy.layer4openvpn"); } public function setLayer4OpenvpnAction($uuid) diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php index b1366ab72..88de730d2 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php @@ -36,12 +36,13 @@ class Layer4Controller extends IndexController { public function indexAction() { - $this->view->pick('OPNsense/Caddy/layer4'); + $this->view->entrypoint = 'layer4'; + $this->view->pick('OPNsense/Caddy/reverse_proxy'); $this->view->formDialogLayer4 = $this->getForm('dialogLayer4'); - $this->view->formGridLayer4 = $this->getFormGrid('dialogLayer4', null, 'ConfChangeMessage'); + $this->view->formGridLayer4 = $this->getFormGrid('dialogLayer4', 'layer4'); $this->view->formDialogLayer4Openvpn = $this->getForm('dialogLayer4Openvpn'); - $this->view->formGridLayer4Openvpn = $this->getFormGrid('dialogLayer4Openvpn', null, 'ConfChangeMessage'); + $this->view->formGridLayer4Openvpn = $this->getFormGrid('dialogLayer4Openvpn', 'layer4_openvpn'); } } diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php index e30315299..49a08c4c4 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php @@ -36,24 +36,25 @@ class ReverseProxyController extends IndexController { public function indexAction() { + $this->view->entrypoint = 'reverse_proxy'; $this->view->pick('OPNsense/Caddy/reverse_proxy'); $this->view->formDialogReverseProxy = $this->getForm("dialogReverseProxy"); - $this->view->formGridReverseProxy = $this->getFormGrid('dialogReverseProxy', null, 'ConfChangeMessage'); + $this->view->formGridReverseProxy = $this->getFormGrid('dialogReverseProxy', 'reverse_proxy'); $this->view->formDialogSubdomain = $this->getForm("dialogSubdomain"); - $this->view->formGridSubdomain = $this->getFormGrid('dialogSubdomain', null, 'ConfChangeMessage'); + $this->view->formGridSubdomain = $this->getFormGrid('dialogSubdomain', 'subdomain'); $this->view->formDialogHandle = $this->getForm("dialogHandle"); - $this->view->formGridHandle = $this->getFormGrid('dialogHandle', null, 'ConfChangeMessage'); + $this->view->formGridHandle = $this->getFormGrid('dialogHandle', 'handle'); $this->view->formDialogAccessList = $this->getForm("dialogAccessList"); - $this->view->formGridAccessList = $this->getFormGrid('dialogAccessList', null, 'ConfChangeMessage'); + $this->view->formGridAccessList = $this->getFormGrid('dialogAccessList', 'access_list'); $this->view->formDialogBasicAuth = $this->getForm("dialogBasicAuth"); - $this->view->formGridBasicAuth = $this->getFormGrid('dialogBasicAuth', null, 'ConfChangeMessage'); + $this->view->formGridBasicAuth = $this->getFormGrid('dialogBasicAuth', 'basic_auth'); $this->view->formDialogHeader = $this->getForm("dialogHeader"); - $this->view->formGridHeader = $this->getFormGrid('dialogHeader', null, 'ConfChangeMessage'); + $this->view->formGridHeader = $this->getFormGrid('dialogHeader', 'header'); } } diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml index 774ebd997..9c9fe2077 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml @@ -44,6 +44,16 @@ false + + accesslist.RequestMatcher + + dropdown + + true + + false + + accesslist.description diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml index 955479b26..72862167b 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml @@ -10,6 +10,9 @@ text + + true + basicauth.description diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml index 3efcdb8dd..3e72c80e2 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml @@ -5,17 +5,11 @@ checkbox - 6em + 20 boolean rowtoggle - - handle.description - - text - - header @@ -25,17 +19,22 @@ dropdown + + false + handle.subdomain dropdown + + false + header - true handle.HandleType @@ -53,7 +52,6 @@ text any - true false @@ -61,14 +59,26 @@ header - true + true + handle.accesslist dropdown + - true + + false + + + + handle.basicauth + + select_multiple + 5 + + false @@ -77,8 +87,8 @@ handle.ForwardAuth checkbox + - true false boolean @@ -97,15 +107,16 @@ header - + + true + handle.HttpVersion dropdown - + - true false @@ -116,9 +127,8 @@ dropdown select_multiple 5 - + - true false @@ -130,16 +140,22 @@ 120 - true false + + header + + handle.HttpTls dropdown + + false + handle.ToDomain @@ -148,19 +164,24 @@ true + + to_domain + handle.ToPort text + + false + handle.ToPath text - true false @@ -169,7 +190,7 @@ handle.HttpTlsInsecureSkipVerify checkbox - + false @@ -181,7 +202,7 @@ handle.HttpTlsTrustedCaCerts dropdown - + false @@ -191,7 +212,7 @@ handle.HttpTlsServerName text - + false @@ -201,7 +222,7 @@ handle.HttpNtlm checkbox - + false @@ -209,18 +230,24 @@ boolean + + handle.description + + text + + header - true + true + handle.lb_policy dropdown - + - true false @@ -232,7 +259,6 @@ off - true false @@ -244,7 +270,6 @@ 0 - true false @@ -256,7 +281,6 @@ 250 - true false @@ -268,7 +292,6 @@ off - true false @@ -280,7 +303,6 @@ 1 - true false @@ -292,7 +314,6 @@ off - true false @@ -304,7 +325,6 @@ off - true false @@ -316,9 +336,115 @@ off - true false + + handle.health_uri + + text + + + + false + + + + handle.health_upstream + + text + + + + false + + + + handle.health_port + + text + + + + false + + + + handle.health_interval + + text + + 30 + + + false + + + + handle.health_passes + + text + + 1 + + + false + + + + handle.health_fails + + text + + 1 + + + false + + + + handle.health_timeout + + text + + 5 + + + false + + + + handle.health_status + + text + + 200 + + + false + + + + handle.health_body + + text + + + + false + + + + handle.health_follow_redirects + + checkbox + + + + false + boolean + boolean + + diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHeader.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHeader.xml index f7885bedd..163b64451 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHeader.xml +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHeader.xml @@ -4,6 +4,9 @@ dropdown + + 120 + header.HeaderType diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml index 9cabfaf42..259cca425 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml @@ -5,7 +5,7 @@ checkbox - 6em + 20 boolean rowtoggle @@ -15,12 +15,9 @@ text - - - layer4.description - - text - + + false + header @@ -39,8 +36,11 @@ layer4.Protocol dropdown - + + + false + layer4.FromPort @@ -66,7 +66,7 @@ layer4.FromDomain select_multiple - + true @@ -74,7 +74,7 @@ layer4.FromOpenvpnModes dropdown - + false @@ -84,7 +84,7 @@ layer4.FromOpenvpnStaticKey select_multiple - + Any 5 @@ -108,7 +108,7 @@ layer4.TerminateTls checkbox - + false @@ -127,35 +127,45 @@ true + + to_domain + layer4.ToPort text + + false + layer4.ProxyProtocol dropdown - true false + + layer4.description + + text + + header - true + true layer4.lb_policy dropdown - + - true false @@ -164,7 +174,6 @@ layer4.PassiveHealthFailDuration text - off true @@ -176,7 +185,6 @@ layer4.PassiveHealthMaxFails text - 1 true diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml index a8163b33f..ed431a6db 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml @@ -5,17 +5,11 @@ checkbox - 6em + 20 boolean rowtoggle - - reverse.description - - text - - header @@ -25,25 +19,34 @@ dropdown + + false + reverse.FromDomain text + + from_domain + reverse.FromPort text + + false + reverse.CustomCertificate dropdown - - + + false @@ -62,7 +65,7 @@ reverse.DnsChallenge checkbox - + false @@ -70,6 +73,16 @@ boolean + + reverse.DnsChallengeOverrideDomain + + text + + true + + false + + reverse.DynDns @@ -81,6 +94,12 @@ boolean + + reverse.description + + text + + header @@ -104,6 +123,25 @@ false + + reverse.ClientAuthTrustPool + + select_multiple + + + false + + + + reverse.ClientAuthMode + + dropdown + true + + + false + + reverse.AccessLog diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml index 5db43f6f2..4cdceffe3 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml @@ -5,17 +5,11 @@ checkbox - 6em + 20 boolean rowtoggle - - subdomain.description - - text - - header @@ -25,12 +19,14 @@ dropdown + + false + subdomain.FromDomain text - opn.example.com @@ -54,6 +50,12 @@ false + + subdomain.description + + text + + header @@ -77,4 +79,23 @@ false + + subdomain.ClientAuthTrustPool + + select_multiple + + + false + + + + subdomain.ClientAuthMode + + dropdown + true + + + false + + diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml index 9d70fcafa..bb7d29b7b 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml @@ -26,12 +26,20 @@ + + header + + caddy.general.DisableSuperuser dropdown + + header + + caddy.general.HttpVersions @@ -52,11 +60,25 @@ 443 + + header + + caddy.general.accesslist dropdown - + + + + caddy.general.ClientIpHeaders + + dropdown + select_multiple + 5 + + X-Forwarded-For + caddy.general.GracePeriod @@ -65,6 +87,34 @@ 10 + + caddy.general.timeout_read_body + + text + no timout + + + + caddy.general.timeout_read_header + + text + no timout + + + + caddy.general.timeout_write + + text + no timout + + + + caddy.general.timeout_idle + + text + 300 + + @@ -98,47 +148,13 @@ caddy.general.TlsDnsProvider dropdown - - - - header - + caddy.general.TlsDnsApiKey - + text - - - - caddy.general.TlsDnsSecretApiKey - - text - - - - caddy.general.TlsDnsOptionalField1 - - text - - - - caddy.general.TlsDnsOptionalField2 - - text - - - - caddy.general.TlsDnsOptionalField3 - - text - - - - caddy.general.TlsDnsOptionalField4 - - text - + header @@ -154,44 +170,70 @@ caddy.general.TlsDnsPropagationTimeout checkbox - + + + + caddy.general.TlsDnsPropagationTimeoutPeriod + + text + 120 + + + + caddy.general.TlsDnsPropagationDelay + + text + 0 + + + + header + + + + caddy.general.TlsDnsEchDomain + + text + + + + header + - - caddy.general.DynDnsIpVersions - + dropdown caddy.general.DynDnsUpdateOnly - + checkbox caddy.general.DynDnsInterval - + text 1800 caddy.general.DynDnsTtl - + text caddy.general.DynDnsSimpleHttp - + text caddy.general.DynDnsInterface - + dropdown @@ -228,11 +270,13 @@ - caddy.general.AuthCopyHeaders + caddy.general.CopyHeaders + dropdown select_multiple + 5 - + general-settings diff --git a/www/caddy/src/opnsense/mvc/app/library/OPNsense/System/Status/CaddyOverrideStatus.php b/www/caddy/src/opnsense/mvc/app/library/OPNsense/System/Status/CaddyOverrideStatus.php index 0acbacf7b..73b383cb0 100644 --- a/www/caddy/src/opnsense/mvc/app/library/OPNsense/System/Status/CaddyOverrideStatus.php +++ b/www/caddy/src/opnsense/mvc/app/library/OPNsense/System/Status/CaddyOverrideStatus.php @@ -1,4 +1,5 @@ - - Services: Caddy Web Server: General Settings - Allow access to Caddy General Settings + + Services: Caddy + Allow access to Caddy - ui/caddy/general/* - api/caddy/general/* + ui/caddy/* + ui/diagnostics/log/core/caddy + api/caddy/* + api/diagnostics/log/core/caddy/* - - - Services: Caddy Web Server: Reverse Proxy - Allow access to Caddy Reverse Proxy - - ui/caddy/reverse_proxy/* - api/caddy/reverse_proxy/* - - - - Services: Caddy Web Server: Layer4 Proxy - Allow access to Caddy Layer4 Proxy - - ui/caddy/layer4/* - api/caddy/reverse_proxy/* - - - - Services: Caddy Web Server: Diagnostics - Allow access to Caddy Diagnostics - - ui/caddy/diagnostics/* - api/caddy/diagnostics/* - - + diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml index 88b4853da..8a1ffeb46 100644 --- a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml @@ -1,7 +1,7 @@ //Pischem/caddy Caddy Reverse Proxy - 1.3.4 + 1.3.7 @@ -25,55 +25,24 @@ None (default) Cloudflare - Duck DNS - Gandi - IONOS - Desec - Porkbun - ACME-DNS - Azure - OVH - Namecheap - PowerDNS - Linode - Hexonet - Mail-in-a-Box - RFC2136 - DNS Made Easy - Bunny - Scaleway - ACME Proxy - INWX - Netcup - Name.com - Infomaniak - DirectAdmin - Vultr - Hetzner - DigitalOcean (optional) - Route53 (optional) - Google Cloud DNS (optional) - Netlify (optional) - DDNSS (optional) - Njalla (optional) - Tencent Cloud (optional) - Dinahosting (optional) - Civo (optional) - EasyDNS (optional) - Hosttech (optional) - ClouDNS (optional) - - - - - + + 1 + Please enter a minimum number of 1 or leave empty for default. + + + 1 + Please enter a minimum number of 1 or leave empty for default. + N + + N + @@ -84,6 +53,17 @@ + + + + OPNsense.Caddy.Caddy + reverseproxy.header + HeaderType,description + %s %s + + + Y + Y 0 @@ -96,7 +76,7 @@ 10 1 20 - Please enter a valid Grace Period between 1 and 20 seconds. + Please enter a valid grace period between 1 and 20 seconds. Y @@ -109,6 +89,22 @@

        HTTP/3

        + + 0 + Please enter a minimum value of 0 or leave empty for defaults. + + + 0 + Please enter a minimum value of 0 or leave empty for defaults. + + + 0 + Please enter a minimum value of 0 or leave empty for defaults. + + + 0 + Please enter a minimum value of 0 or leave empty for defaults. + @@ -166,29 +162,19 @@ /^(\/.*)?$/u - Please enter a valid 'URI' that starts with '/'. + Please enter a valid URI that starts with '/'. - + + + + OPNsense.Caddy.Caddy + reverseproxy.header + HeaderType,description + %s %s + + Y - - Authorization - Remote-User - Remote-Groups - Remote-Name - Remote-Email - X-Authentik-Username - X-Authentik-Groups - X-Authentik-Email - X-Authentik-Name - X-Authentik-Uid - X-Authentik-Jwt - X-Authentik-Meta-Jwks - X-Authentik-Meta-Outpost - X-Authentik-Meta-Provider - X-Authentik-Meta-App - X-Authentik-Meta-Version - - +
        @@ -226,8 +212,12 @@ + + N + Please enter a valid domain name. + - ACME + Auto HTTPS @@ -240,6 +230,18 @@ http:// + + require_and_verify + + request + require + verify_if_given + + + + ca + Y + @@ -254,6 +256,9 @@ reverseproxy.reverse FromDomain,FromPort %s %s + + /^\*\./ +
        @@ -286,6 +291,18 @@ + + require_and_verify + + request + require + verify_if_given + + + + ca + Y + @@ -323,7 +340,7 @@ /^(\/.*)?$/u - Please enter a valid Path that starts with '/'. + Please enter a valid path that starts with '/'. @@ -335,6 +352,17 @@ + + + + OPNsense.Caddy.Caddy + reverseproxy.basicauth + basicauthuser,description + %s %s + + + Y +
        @@ -356,14 +384,13 @@ Y - , Y Please enter one or multiple valid IP addresses, hostnames or FQDNs. /^(\/.*)?$/u - Please enter a valid Path that starts with '/'. + Please enter a valid path that starts with '/'. @@ -437,7 +464,7 @@ Please enter a minimum value of 2 or leave empty for defaults. - /^(100|[1-5][0-9]{2}|[1-5]xx)$/u + /^(100|[1-5][0-9]{2}|[1-5]xx)$/u Please enter a valid HTTP response code like 404 a status code class like 4xx. @@ -449,6 +476,38 @@ Please enter a minimum value of 1 or leave empty for defaults. + + /^(\/.*)?$/u + Please enter a valid URI that starts with '/'. + + + + 1 + 65535 + Please enter a minimum value of 1-65535 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + /^(100|[1-5][0-9]{2}|[1-5]xx)$/u + Please enter a 3-digit status code or a status code ending in xx or leave empty for defaults. + + + @@ -456,7 +515,6 @@ Y - , Y Y Please enter one or multiple valid IP addresses or networks. @@ -465,9 +523,17 @@ 100 599 - Please enter a valid HTTP response code between 100 and 599 + Please enter a valid HTTP response code between 100 and 599. + + Y + client_ip + + client_ip + remote_ip + + @@ -542,7 +608,6 @@ N Y Y - , Y Please enter one or multiple hostnames or FQDNs. @@ -596,7 +661,6 @@ Y - , Y Please enter one or multiple valid IP addresses, hostnames or FQDNs. @@ -631,7 +695,6 @@ Please enter a minimum value of 2 or leave empty for defaults. - , Y Y Please enter one or multiple valid IP addresses or networks. diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt index ef58bada0..fd56411d1 100644 --- a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt +++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt @@ -1,5 +1,5 @@ {# - # Copyright (c) 2023-2024 Cedrik Pischem + # Copyright (c) 2023-2025 Cedrik Pischem # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -49,39 +49,6 @@ }); } - /** - * Manages the spinner icon. - * @param {string} generalId - The ID of the general form. - * @param {string} action - The action to perform ("start" or "stop"). - */ - function setSpinner(generalId, action) { - const $progressIcon = $("#" + generalId + "_progress"); - if (action === 'start') { - $progressIcon.addClass("fa fa-spinner fa-pulse"); - } else if (action === 'stop') { - $progressIcon.removeClass("fa fa-spinner fa-pulse"); - } - } - - /** - * Reconfigures the service. - * @param {string} generalId - The ID of the general form (used for controlling the spinner). - */ - function reconfigureService(generalId) { - ajaxCall("/api/caddy/service/reconfigure", {}, function(data, status) { - if (status !== "success" || data.status !== 'ok') { - showAlert("{{ lang._('Error applying configuration: ') }}" + JSON.stringify(data), "error"); - } else { - showAlert("{{ lang._('Configuration applied successfully.') }}", "success"); - updateServiceControlUI('caddy'); - } - setSpinner(generalId, 'stop'); - }).fail(function() { - handleError("error", "{{ lang._('Reconfiguration request failed.') }}"); - setSpinner(generalId, 'stop'); - }); - } - /** * Centralizes error handling. * @param {string} type - The type of error. @@ -92,27 +59,48 @@ } // Event binding for saving forms - $('[id*="save_general-"]').on('click', function() { - const generalId = this.form.id; - setSpinner(generalId, 'start'); - saveFormToEndpoint("/api/caddy/general/set", generalId, function() { - // Validate Caddyfile after saving the form - ajaxGet("/api/caddy/service/validate", {}, function(data, status) { - if (status === "success" && data && data.status.toLowerCase() === 'ok') { - reconfigureService(generalId); - } else { - handleError("error", data.message || "{{ lang._('Validation Error') }}"); - setSpinner(generalId, 'stop'); - } - }).fail(function() { - handleError("error", "{{ lang._('Validation request failed.') }}"); - setSpinner(generalId, 'stop'); - }); - }, true, function(errorData) { - handleError("error", errorData.message || "{{ lang._('Validation Error') }}"); - setSpinner(generalId, 'stop'); + $('[id^="save_general-"]').each(function () { + const $btn = $(this); + const formId = this.id.replace(/^save_/, 'frm_'); + + $btn.attr({ + 'data-label' : "{{ lang._('Apply') }}", + 'data-endpoint' : "/api/caddy/service/reconfigure", + 'data-service-widget' : "caddy" + }); + + $btn.SimpleActionButton({ + onPreAction: function () { + const dfObj = new $.Deferred(); + + saveFormToEndpoint( + "/api/caddy/general/set", + formId, + function () { + ajaxGet("/api/caddy/service/validate", null, function (data, status) { + if (status === "success" && data && data['status'].toLowerCase() === 'ok') { + dfObj.resolve(); + } else { + showAlert(data?.message || "{{ lang._('Validation Error') }}", "error"); + dfObj.reject(); + } + }).fail(function(xhr, status, error) { + showAlert("{{ lang._('Validation request failed: ') }}" + error, "error"); + dfObj.reject(); + }); + }, + true, + function (errorData) { + showAlert(errorData.message || "{{ lang._('Validation Error') }}", "error"); + dfObj.reject(); + } + ); + + return dfObj.promise(); + } }); }); + }); diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/layer4.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/layer4.volt deleted file mode 100644 index be371635c..000000000 --- a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/layer4.volt +++ /dev/null @@ -1,181 +0,0 @@ -{# - # Copyright (c) 2024-2025 Cedrik Pischem - # 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. - #} - - - - - - - -
        - -
        -
        -

        {{ lang._('Layer4 Routes') }}

        -
        - {{ partial('layout_partials/base_bootgrid_table', formGridLayer4)}} -
        -
        -
        - - -
        -
        - -

        {{ lang._('OpenVPN Static Keys') }}

        -
        - {{ partial('layout_partials/base_bootgrid_table', formGridLayer4Openvpn)}} -
        -
        -
        -
        - - - -
        -
        -
        -
        - -

        - - - - -
        -
        -
        - -{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4,'id':formGridLayer4['edit_dialog_id'],'label':lang._('Edit Layer4 Route')])}} -{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4Openvpn,'id':formGridLayer4Openvpn['edit_dialog_id'],'label':lang._('Edit OpenVPN Static Key')])}} diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt index f95594d21..bf6de6252 100644 --- a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt +++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt @@ -26,89 +26,255 @@ + +
        - -
        - -
        - - -
        - - -
        + +{% if entrypoint == 'reverse_proxy' %} -
        -
        - -

        {{ lang._('Domains') }}

        -
        - {{ partial('layout_partials/base_bootgrid_table', formGridReverseProxy)}} -
        -
        +
        + +

        {{ lang._('Domains') }}

        + {{ partial('layout_partials/base_bootgrid_table', formGridReverseProxy + {'command_width': '160'})}} -
        -

        {{ lang._('Subdomains') }}

        -
        - {{ partial('layout_partials/base_bootgrid_table', formGridSubdomain)}} -
        -
        +

        {{ lang._('Subdomains') }}

        + {{ partial('layout_partials/base_bootgrid_table', formGridSubdomain + {'command_width': '160'})}}
        -
        -
        -

        {{ lang._('Handlers') }}

        -
        - {{ partial('layout_partials/base_bootgrid_table', formGridHandle)}} -
        -
        +
        +

        {{ lang._('Handlers') }}

        + {{ partial('layout_partials/base_bootgrid_table', formGridHandle)}}
        -
        +
        -
        -

        {{ lang._('Access Lists') }}

        -
        - {{ partial('layout_partials/base_bootgrid_table', formGridAccessList)}} -
        -
        +

        {{ lang._('Access Lists') }}

        + {{ partial('layout_partials/base_bootgrid_table', formGridAccessList)}} -
        -

        {{ lang._('Basic Auth') }}

        -
        - {{ partial('layout_partials/base_bootgrid_table', formGridBasicAuth)}} -
        -
        +

        {{ lang._('Basic Auth') }}

        + {{ partial('layout_partials/base_bootgrid_table', formGridBasicAuth)}}
        -
        -
        -

        {{ lang._('Headers') }}

        -
        - {{ partial('layout_partials/base_bootgrid_table', formGridHeader)}} -
        -
        +
        +

        {{ lang._('Headers') }}

        + {{ partial('layout_partials/base_bootgrid_table', formGridHeader)}}
        + +{% elseif entrypoint == 'layer4' %} + + +
        +

        {{ lang._('Layer4 Routes') }}

        + {{ partial('layout_partials/base_bootgrid_table', formGridLayer4)}} +
        + + +
        +

        {{ lang._('OpenVPN Static Keys') }}

        + {{ partial('layout_partials/base_bootgrid_table', formGridLayer4Openvpn)}} +
        + +{% endif %} +
        - -
        -
        -
        -
        - -

        - - - - -
        -
        -
        +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/caddy/service/reconfigure', 'data_service_widget': 'caddy'}) }} + +{% if entrypoint == 'reverse_proxy' %} {{ partial("layout_partials/base_dialog",['fields':formDialogReverseProxy,'id':formGridReverseProxy['edit_dialog_id'],'label':lang._('Edit Domain')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogSubdomain,'id':formGridSubdomain['edit_dialog_id'],'label':lang._('Edit Subdomain')])}} @@ -427,3 +563,10 @@ {{ partial("layout_partials/base_dialog",['fields':formDialogAccessList,'id':formGridAccessList['edit_dialog_id'],'label':lang._('Edit Access List')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogBasicAuth,'id':formGridBasicAuth['edit_dialog_id'],'label':lang._('Edit Basic Auth')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogHeader,'id':formGridHeader['edit_dialog_id'],'label':lang._('Edit Header')])}} + +{% elseif entrypoint == 'layer4' %} + +{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4,'id':formGridLayer4['edit_dialog_id'],'label':lang._('Edit Layer4 Route')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4Openvpn,'id':formGridLayer4Openvpn['edit_dialog_id'],'label':lang._('Edit OpenVPN Static Key')])}} + +{% endif %} diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php index 67618d75a..bcf424ece 100755 --- a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php @@ -43,7 +43,8 @@ $writeFileIfChanged = function ($filePath, $content) { } }; -$tempDir = '/var/db/caddy/data/caddy/certificates/temp/'; +/* XXX used later to only append a file name */ +$tempDir = '/usr/local/etc/caddy/certificates/'; // leaf certificate chain $certificateRefs = []; @@ -87,14 +88,38 @@ foreach ((new Caddy())->reverseproxy->handle->iterateItems() as $handleItem) { } } +foreach ((new Caddy())->reverseproxy->reverse->iterateItems() as $reverseItem) { + $caCertField = (string)$reverseItem->ClientAuthTrustPool; + + if (!empty($caCertField)) { + $refs = array_map('trim', explode(',', $caCertField)); + foreach ($refs as $ref) { + if (!empty($ref)) { + $caCertRefs[] = $ref; + } + } + } +} + +foreach ((new Caddy())->reverseproxy->subdomain->iterateItems() as $subdomainItem) { + $caCertField = (string)$subdomainItem->ClientAuthTrustPool; + + if (!empty($caCertField)) { + $refs = array_map('trim', explode(',', $caCertField)); + foreach ($refs as $ref) { + if (!empty($ref)) { + $caCertRefs[] = $ref; + } + } + } +} + $caCertRefs = array_unique($caCertRefs); foreach ((new Ca())->ca->iterateItems() as $caItem) { $refid = (string)$caItem->refid; - if (in_array($refid, $caCertRefs, true)) { $caCert = base64_decode((string)$caItem->crt); - $writeFileIfChanged($tempDir . $refid . '.pem', $caCert); } } diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py index 9a15eb01f..27f933d99 100755 --- a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py @@ -30,8 +30,6 @@ import sys import json import os import subprocess -import asyncio -from datetime import datetime # Function to show the Caddy configuration from a JSON file @@ -69,88 +67,11 @@ def show_caddyfile(): print(json.dumps({"error": "General Error", "message": str(e)})) -# Function to extract certificate information using openssl command -async def extract_certificate_info(cert_path): - try: - # Execute the openssl command to get the expiration date with a timeout - result = await asyncio.wait_for( - asyncio.create_subprocess_exec( - 'openssl', 'x509', '-in', cert_path, '-noout', '-enddate', - stdout=subprocess.PIPE, stderr=subprocess.PIPE), - timeout=10) # Make sure tasks are cleaned up if they hang - - stdout, stderr = await result.communicate() - - # Check for errors in the execution - if result.returncode != 0: - error_message = stderr.decode().strip() - raise RuntimeError(f"Subprocess failed with error: {error_message}") - - # Decode output and process the information - expiration_date_str = stdout.decode().strip().split('=')[1] - - # Convert expiration date string to datetime object - expiration_date = datetime.strptime(expiration_date_str, "%b %d %H:%M:%S %Y GMT") - - # Determine the current date - now = datetime.now() - - # Calculate remaining days until expiration - remaining_days = (expiration_date - now).days - remaining_days = max(remaining_days, 0) # Ensure non-negative days - - # Extract the hostname from the filename - hostname = os.path.basename(cert_path).replace('.crt', '').lower() - if hostname.startswith("wildcard_"): - hostname = hostname.replace("wildcard_", "*", 1) - - return {'hostname': hostname, 'expiration_date': expiration_date_str, 'remaining_days': remaining_days} - except asyncio.TimeoutError as e: - # Handle timeout specific errors - raise RuntimeError(f"Timeout occurred while processing {cert_path}: {str(e)}") - except Exception as e: - raise RuntimeError(f"Error extracting certificate info for {cert_path}: {str(e)}") - - -# Function to find certificates and create tasks to extract info -async def find_certificates(base_dir): - tasks = [] - for root, dirs, files in os.walk(base_dir): - # Skip any directories named 'temp' - dirs[:] = [d for d in dirs if d != 'temp'] - for file in files: - if file.endswith('.crt'): - cert_path = os.path.join(root, file) - task = asyncio.create_task(extract_certificate_info(cert_path)) - tasks.append(task) - - if not tasks: - raise RuntimeError("No certificates were found in the specified directory.") - - results = await asyncio.gather(*tasks, return_exceptions=True) - return [result for result in results if not isinstance(result, Exception)] - - -# Function to show certificates, processing all found in the given directory -async def show_certificates(): - # Function to show certificates, processing all found in the given directory - base_dir = '/var/db/caddy/data/caddy/certificates' - try: - certificates_data = await find_certificates(base_dir) - if certificates_data: - print(json.dumps(certificates_data)) - else: - raise RuntimeError("No valid certificate data found.") - except Exception as e: - print(json.dumps({"error": "General Error", "message": str(e)})) - - # Action handler def perform_action(cmd_action): actions = { "config": show_caddy_config, - "caddyfile": show_caddyfile, - "certificate": lambda: asyncio.run(show_certificates()) + "caddyfile": show_caddyfile } action_func = actions.get(cmd_action) diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh index e2d00c66a..8cbc15e24 100755 --- a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright (c) 2023-2024 Cedrik Pischem +# Copyright (c) 2023-2025 Cedrik Pischem # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -26,46 +26,36 @@ # POSSIBILITY OF SUCH DAMAGE. # -# The directories are created as root:www with rwx permissions for both, -# so the user can change in the GUI if caddy runs as root or www -# If only ports 1024 and above are used, caddy can run as www user and group. +# Detect configured caddy_user/group (defaults) +. /etc/rc.conf.d/caddy +CADDY_USER="${caddy_user:-root}" +CADDY_GROUP="${caddy_group:-wheel}" # Define directories CADDY_CONF_DIR="/usr/local/etc/caddy" CADDY_DATA_DIR="/var/db/caddy" CADDY_LOG_DIR="/var/log/caddy" -CADDY_RUN_DIR="/var/run/caddy" CADDY_CONF_CUSTOM_DIR="${CADDY_CONF_DIR}/caddy.d" -CADDY_DATA_CUSTOM_DIR="${CADDY_DATA_DIR}/data/caddy/certificates/temp" +CADDY_CONF_CERT_DIR="${CADDY_CONF_DIR}/certificates" CADDY_LOG_CUSTOM_DIR="${CADDY_LOG_DIR}/access" -mkdir -p "${CADDY_CONF_DIR}" -mkdir -p "${CADDY_DATA_DIR}" -mkdir -p "${CADDY_LOG_DIR}" -mkdir -p "${CADDY_RUN_DIR}" -mkdir -p "${CADDY_CONF_CUSTOM_DIR}" -mkdir -p "${CADDY_DATA_CUSTOM_DIR}" -mkdir -p "${CADDY_LOG_CUSTOM_DIR}" +# Group the main directories +CADDY_DIRS=" +${CADDY_CONF_DIR} +${CADDY_DATA_DIR} +${CADDY_LOG_DIR} +${CADDY_CONF_CERT_DIR} +${CADDY_CONF_CUSTOM_DIR} +${CADDY_LOG_CUSTOM_DIR} +" -chown -R root:www "${CADDY_CONF_DIR}" -chown -R root:www "${CADDY_DATA_DIR}" -chown -R root:www "${CADDY_LOG_DIR}" -chown -R root:www "${CADDY_RUN_DIR}" +# No inode changes occur when directory already exists or permissions are correct. +# Always running these when caddy starts guarantees correct ownership with minimal read and writes. +mkdir -p ${CADDY_DIRS} +chown -R "${CADDY_USER}:${CADDY_GROUP}" ${CADDY_DIRS} -# Directories need execute permissions to be accessible -find "${CADDY_CONF_DIR}" -type d -exec chmod 770 {} + -find "${CADDY_DATA_DIR}" -type d -exec chmod 770 {} + -find "${CADDY_LOG_DIR}" -type d -exec chmod 770 {} + -find "${CADDY_RUN_DIR}" -type d -exec chmod 770 {} + +# Format and overwrite the Caddyfile +( cd "${CADDY_CONF_DIR}" && /usr/local/bin/caddy fmt --overwrite ) -# Files can have read/write permissions -find "${CADDY_CONF_DIR}" -type f -exec chmod 660 {} + -find "${CADDY_DATA_DIR}" -type f -exec chmod 660 {} + -find "${CADDY_LOG_DIR}" -type f -exec chmod 660 {} + -find "${CADDY_RUN_DIR}" -type f -exec chmod 660 {} + - -# Format and overwrite the Caddyfile, this makes whitespace control in jinja2 unnecessary -(cd "${CADDY_CONF_DIR}" && /usr/local/bin/caddy fmt --overwrite) - -# Write custom certs from the OPNsense Trust Store to CADDY_DATA_CUSTOM_DIR +# Write custom certs from the OPNsense Trust Store /usr/local/opnsense/scripts/OPNsense/Caddy/caddy_certs.php diff --git a/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf b/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf index be0997b93..0d768ce42 100644 --- a/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf +++ b/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf @@ -47,9 +47,3 @@ command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py caddyfil parameters: type:script_output message:Request Caddyfile - -[certificate] -command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py certificate -parameters: -type:script_output -message:Check validity of automatic certificates diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile index e81c23bc6..c3f5a3862 100644 --- a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile @@ -1,5 +1,5 @@ {# -# Copyright (c) 2023-2024 Cedrik Pischem +# Copyright (c) 2023-2025 Cedrik Pischem # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -43,13 +43,6 @@ # with the syslog-ng instance running on the OPNsense. #} log { - {% if generalSettings.LogAccessPlain|default("0") == "0" %} - {% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %} - {% if reverse.enabled|default("0") == "1" and reverse.AccessLog|default("0") == "1" %} - include http.log.access.{{ reverse['@uuid'] }} - {% endif %} - {% endfor %} - {% endif %} output net unixgram//var/run/caddy/log.sock { } format json { @@ -85,9 +78,37 @@ {% if accessList %} trusted_proxies static {{ accessList.clientIps.split(',') | join(' ') }} {% endif %} + {% if generalSettings.ClientIpHeaders %} + {% for header_uuid in generalSettings.ClientIpHeaders.split(',') %} + {% set header = helpers.toList('Pischem.caddy.reverseproxy.header') | selectattr('@uuid', 'equalto', header_uuid) | first %} + {% if header and header.HeaderType %} + client_ip_headers {{ header.HeaderType }} + {% endif %} + {% endfor %} + {% endif %} + {% if generalSettings.LogCredentials|default("0") == "1" %} log_credentials {% endif %} + {% if generalSettings.timeout_read_body or + generalSettings.timeout_read_header or + generalSettings.timeout_write or + generalSettings.timeout_idle %} + timeouts { + {% if generalSettings.timeout_read_body %} + read_body {{ generalSettings.timeout_read_body }}s + {% endif %} + {% if generalSettings.timeout_read_header %} + read_header {{ generalSettings.timeout_read_header }}s + {% endif %} + {% if generalSettings.timeout_write %} + write {{ generalSettings.timeout_write }}s + {% endif %} + {% if generalSettings.timeout_idle %} + idle {{ generalSettings.timeout_idle }}s + {% endif %} + } + {% endif %} {% if generalSettings.EnableLayer4|default("0") == "1" %} listener_wrappers { layer4 { @@ -114,29 +135,9 @@ # Purpose: Sets up global configuration for Dynamic DNS. Caddy needs to be compiled with # https://github.com/mholt/caddy-dynamicdns and https://github.com/caddy-dns. Otherwise the # generated Caddyfile won't run. Each DNS Provider that is added below has to be compiled in. - # Some Providers don't support setting A and AAAA-Records, like acmedns. - # Most need specific configurations. Since only one provider can be used at the same time, - # they all share the same fields for configuration. - # Parameters: - # - @param dnsProvider (string): Specifies the DNS provider for DDNS updates. - # - @param dnsApiKey (string): The API key for authenticating with the DNS provider. - # - @param dnsSecretApiKey (string): A secret API key or token for additional authentication security. - # - @param dnsOptionalField1 to 4 (string): Optional configuration field for the DNS provider. - # - @param dynDnsSimpleHttp (string): URL for a simple HTTP-based service to discover the server's public IP. - # - @param dynDnsInterface (string): Network interface(s) to use for IP discovery. - # - @param dynDnsCheckInterval (integer): Interval in seconds to check for IP changes. Can be empty for defaults. - # - @param dynDnsIpVersions (string): The IP version(s) (IPv4, IPv6) for the DDNS update. - # - @param dynDnsTtl (integer): Time-To-Live for the DNS records, in seconds. Can be empty for defaults. - # - @param dynDnsDomains (list): Domains and subdomains list for which DDNS updates are enabled. - # - @param dynDnsUpdateOnly (boolean): If set, only updates DNS records, not creating new ones. #} {% set dnsProvider = helpers.toList('Pischem.caddy.general.TlsDnsProvider') | first %} {% set dnsApiKey = generalSettings.TlsDnsApiKey %} - {% set dnsSecretApiKey = generalSettings.TlsDnsSecretApiKey %} - {% set dnsOptionalField1 = generalSettings.TlsDnsOptionalField1 %} - {% set dnsOptionalField2 = generalSettings.TlsDnsOptionalField2 %} - {% set dnsOptionalField3 = generalSettings.TlsDnsOptionalField3 %} - {% set dnsOptionalField4 = generalSettings.TlsDnsOptionalField4 %} {% set dynDnsSimpleHttp = generalSettings.DynDnsSimpleHttp %} {% set dynDnsInterface = generalSettings.DynDnsInterface %} {% set dynDnsUpdateOnly = generalSettings.DynDnsUpdateOnly %} @@ -166,25 +167,9 @@ {% endfor %} {% endfor %} - {# - # Define special DNS Providers that have more than one API key, or special requirements that do not allow the use of the default. - # The same providers have to be added to "OPNsense/Caddy/includeDnsProvider", best in the same order as in this array for maintainability. - # For a new provider to work, it has to be compiled into the caddy binary. - #} - {% set dnsProviderSpecialConfig = ['duckdns', 'porkbun', 'desec', 'route53', 'acmedns', 'googleclouddns', 'azure', 'ovh', 'namecheap', 'powerdns', 'ddnss', 'linode', 'tencentcloud', 'dinahosting', 'hexonet', 'mailinabox', 'netcup', 'rfc2136', 'dnsmadeeasy', 'civo', 'scaleway', 'acmeproxy', 'inwx', 'namedotcom', 'easydns', 'directadmin', 'cloudns'] %} - - {# Conditionally add the dynamic_dns section, acmedns provider is special, it does not support dynamic_dns. #} - {% if dnsProvider and dynDnsDomains|length > 0 and dnsProvider != "acmedns" %} + {% if dnsProvider and dynDnsDomains|length > 0 %} dynamic_dns { - {# duckdns provider is special, it has a different configuration for dynamic dns than for the dns-01 challenge. #} - {% if dnsProvider in dnsProviderSpecialConfig and dnsProvider != "duckdns" %} - provider {{ dnsProvider }} { - {% include "OPNsense/Caddy/includeDnsProvider" %} - } - {% else %} - {# Other DNS Providers fall under this default #} provider {{ dnsProvider }} {{ dnsApiKey }} - {% endif %} domains { {% for domain in dynDnsDomains %} {{ domain }} @@ -215,6 +200,15 @@ } {% endif %} + {# + # Section: Encrypted ClientHello (ECH) Configuration + # https://caddyserver.com/docs/caddyfile/options#ech + #} + {% if generalSettings.TlsDnsEchDomain|default("") and dnsProvider %} + dns {{ dnsProvider }} {{ dnsApiKey }} + ech {{ generalSettings.TlsDnsEchDomain }} + {% endif %} + {# # Section: ACME Email, Auto HTTPS selection and global import statement # Purpose: The ACME email is optional for receiving certificate notices. @@ -297,46 +291,54 @@ http://{{ domain }} { # Purpose: Configures TLS settings based on the DNS provider, API keys, and optional fields. # Sets up the Caddyfile to update TXT Records with the chosen DNS Provider and receive # certificates with the DNS-01 challenge. Refer to Dynamic DNS section for more details. -# Parameters: -# - @param dnsProvider (string): The DNS provider used for the DNS challenge. -# - @param dnsApiKey (string): API key for the DNS provider, essential for authentication. -# - @param customCert (string, optional): The config extracted name of a certificate. -# - @param dnsChallenge (boolean): Indicates if a DNS challenge is used for certificate authentication. -# - @param dnsSecretApiKey (string, optional): A secret API key or token for additional security, depending on the provider. -# - @param TlsDnsOptionalField1 to 4 (string, optional): Additional fields for specific DNS provider configurations. -# - @param TlsDnsPropagationTimeout (boolean, optional): Disables Propagation Timeout for DNS Challenge. -# - @param TlsDnsPropagationResolvers (string, optional): Set custom nameserver for DNS Challenge. #} {% macro tls_configuration( - customCert, - dnsChallenge, - dnsProvider, - dnsApiKey, - dnsSecretApiKey, - tlsDnsOptionalField1, - tlsDnsOptionalField2, - tlsDnsOptionalField3, - tlsDnsOptionalField4, - tlsDnsPropagationTimeout, - tlsDnsPropagationResolvers + customCert="", + dnsChallenge="0", + dnsChallengeOverrideDomain="", + clientAuthTrustPool="", + clientAuthMode="", + dnsProvider="", + dnsApiKey="", + tlsDnsPropagationTimeout="", + tlsDnsPropagationTimeoutPeriod="", + tlsDnsPropagationDelay="", + tlsDnsPropagationResolvers="" ) %} - {% if customCert or (dnsChallenge == "1" and dnsProvider) %} - tls {% if customCert %}/var/db/caddy/data/caddy/certificates/temp/{{ customCert }}.pem /var/db/caddy/data/caddy/certificates/temp/{{ customCert }}.key{% endif %} {% if not customCert and dnsChallenge == "1" and dnsProvider %}{ + {% if customCert or (dnsChallenge == "1" and dnsProvider) or clientAuthTrustPool %} + tls {% if customCert %}/usr/local/etc/caddy/certificates/{{ customCert }}.pem /usr/local/etc/caddy/certificates/{{ customCert }}.key{% endif %} { + {% if not customCert and (dnsChallenge == "1" and dnsProvider) %} issuer acme { - dns {{ dnsProvider }} {% if dnsProvider not in dnsProviderSpecialConfig %}{{ dnsApiKey }}{% else %}{ - {% include "OPNsense/Caddy/includeDnsProvider" %} - } + dns {{ dnsProvider }} {{ dnsApiKey }} + {% if dnsChallengeOverrideDomain %} + dns_challenge_override_domain {{ dnsChallengeOverrideDomain }} {% endif %} {% if tlsDnsPropagationResolvers %} resolvers {{ tlsDnsPropagationResolvers }} {% endif %} {% if tlsDnsPropagationTimeout|default("0") == "1" %} - propagation_delay 30s propagation_timeout -1 + {% elif tlsDnsPropagationTimeoutPeriod %} + propagation_timeout {{ tlsDnsPropagationTimeoutPeriod }}s + {% endif %} + {% if tlsDnsPropagationDelay %} + propagation_delay {{ tlsDnsPropagationDelay }}s {% endif %} } - }{% endif %} + {% endif %} + + {% if clientAuthTrustPool %} + client_auth { + {% for ca in clientAuthTrustPool.split(',') %} + trust_pool file /usr/local/etc/caddy/certificates/{{ ca.strip() }}.pem + {% endfor %} + {% if clientAuthMode %} + mode {{ clientAuthMode }} + {% endif %} + } + {% endif %} + } {% endif %} {% endmacro %} @@ -392,6 +394,7 @@ http://{{ domain }} { {% macro reverse_proxy_configuration(handle) %} {{ handle.HandleType }} {{ handle.HandlePath|default("") }} { {{ handle_accesslist(handle.accesslist) }} + {{ render_basic_auth(handle.basicauth) }} {# All IPs not matched by accesslist will continue processing #} {% if handle.ForwardAuth|default("0") == "1" %} {% include "OPNsense/Caddy/includeAuthProvider" %} @@ -429,6 +432,36 @@ http://{{ domain }} { {% if handle.lb_try_interval|default("") %} lb_try_interval {{ handle.lb_try_interval }}ms {% endif %} + {% if handle.health_uri|default("") %} + health_uri {{ handle.health_uri }} + {% endif %} + {% if handle.health_upstream|default("") %} + health_upstream {{ handle.health_upstream }} + {% endif %} + {% if handle.health_port|default("") %} + health_port {{ handle.health_port }} + {% endif %} + {% if handle.health_interval|default("") %} + health_interval {{ handle.health_interval }}s + {% endif %} + {% if handle.health_passes|default("") %} + health_passes {{ handle.health_passes }} + {% endif %} + {% if handle.health_fails|default("") %} + health_fails {{ handle.health_fails }} + {% endif %} + {% if handle.health_timeout|default("") %} + health_timeout {{ handle.health_timeout }}s + {% endif %} + {% if handle.health_status|default("") %} + health_status {{ handle.health_status }} + {% endif %} + {% if handle.health_body|default("") %} + health_body {{ handle.health_body }} + {% endif %} + {% if handle.health_follow_redirects|default("0") == "1" %} + health_follow_redirects + {% endif %} {% if handle.PassiveHealthFailDuration|default("") %} fail_duration {{ handle.PassiveHealthFailDuration }}s {% endif %} @@ -472,7 +505,7 @@ http://{{ domain }} { tls_insecure_skip_verify {% endif %} {% if handle.HttpTlsTrustedCaCerts %} - tls_trust_pool file /var/db/caddy/data/caddy/certificates/temp/{{ handle.HttpTlsTrustedCaCerts }}.pem + tls_trust_pool file /usr/local/etc/caddy/certificates/{{ handle.HttpTlsTrustedCaCerts }}.pem {% endif %} {% if handle.HttpTlsServerName %} tls_server_name {{ handle.HttpTlsServerName }} @@ -505,7 +538,7 @@ http://{{ domain }} { {% set client_ips = accesslist_obj.clientIps.split(',') | join(' ') %} {{ unique_identifier }} { {# Non-inverted access lists have "not" as default, inverted ones '' #} - {{ 'not' if accesslist_obj.accesslistInvert|default("0") == "0" else '' }} client_ip {{ client_ips }} + {{ 'not' if accesslist_obj.accesslistInvert|default("0") == "0" else '' }} {{ accesslist_obj.RequestMatcher }} {{ client_ips }} } {# When IP is matched, abort or send response code. This will end processing and drop the request #} handle {{ unique_identifier }} { @@ -540,14 +573,11 @@ http://{{ domain }} { {# # Macro: render_handles -# Purpose: Renders the handles in the correct order (path-specific first, then catch-all), -# including basic authentication configuration. +# Purpose: Renders the handles in the correct order (path-specific first, then catch-all). # Parameters: # @param handles (list): A list of handle objects to be rendered. -# @param basicauth_uuids (string): A comma-separated list of UUIDs for basic authentication. #} -{% macro render_handles(handles, basicauth_uuids=None) %} - {{ render_basic_auth(basicauth_uuids) }} +{% macro render_handles(handles) %} {% for handle in handles %} {% if handle.enabled|default("0") == "1" and handle.HandlePath %} {{ reverse_proxy_configuration(handle) }} @@ -570,17 +600,14 @@ http://{{ domain }} { # - render_handles: Renders the handles in the correct order, including basic authentication. # - reverse_proxy_configuration: Sets up the handle with reverse proxy configurations. # - handle_response: Manages the response logic for access lists and aborts. -# Important Details: -# - Order of Path specific Handles: Prioritizes order of specific path handles over catch-all handles. -# - Order of Wildcard Domains and Subdomains: Handles for wildcard domains come after all subdomains. #} + {% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %} {% if reverse.enabled|default("0") == "1" %} - # Reverse Proxy Domain: "{{ reverse['@uuid'] }}" {% if reverse.DisableTls|default("0") == "1" %}http://{% endif %}{{ reverse.FromDomain|default("") }}{% if reverse.FromPort %}:{{ reverse.FromPort }}{% endif %} { {% if reverse.AccessLog|default("0") == "1" %} {% if generalSettings.LogAccessPlain|default("0") == "0" %} - log {{ reverse['@uuid'] }} + log default {% else %} log { output file /var/log/caddy/access/{{ reverse['@uuid'] }}.log { @@ -589,41 +616,55 @@ http://{{ domain }} { } {% endif %} {% endif %} - {% set customCert = reverse.CustomCertificate|default("") %} - {% set dnsChallenge = reverse.DnsChallenge|default("0") %} {{ tls_configuration( - customCert, - dnsChallenge, - dnsProvider, - dnsApiKey, - dnsSecretApiKey, - tlsDnsOptionalField1, - tlsDnsOptionalField2, - tlsDnsOptionalField3, - tlsDnsOptionalField4, - generalSettings.TlsDnsPropagationTimeout, - generalSettings.TlsDnsPropagationResolvers + customCert=reverse.CustomCertificate|default(""), + dnsChallenge=reverse.DnsChallenge|default("0"), + dnsChallengeOverrideDomain=reverse.DnsChallengeOverrideDomain|default(""), + clientAuthTrustPool=reverse.ClientAuthTrustPool|default(""), + clientAuthMode=reverse.ClientAuthMode|default(""), + dnsProvider=generalSettings.TlsDnsProvider, + dnsApiKey=generalSettings.TlsDnsApiKey, + tlsDnsPropagationTimeout=generalSettings.TlsDnsPropagationTimeout, + tlsDnsPropagationTimeoutPeriod=generalSettings.TlsDnsPropagationTimeoutPeriod, + tlsDnsPropagationDelay=generalSettings.TlsDnsPropagationDelay, + tlsDnsPropagationResolvers=generalSettings.TlsDnsPropagationResolvers ) }} - - {% for subdomain in helpers.toList('Pischem.caddy.reverseproxy.subdomain') %} - {% if subdomain.enabled|default("0") == "1" and subdomain.reverse == reverse['@uuid'] %} - @{{ subdomain['@uuid'] }} { - host {{ subdomain.FromDomain }} - } - handle @{{ subdomain['@uuid'] }} { - {% set subdomain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('subdomain', 'equalto', subdomain['@uuid']) | list %} - {{ handle_accesslist(subdomain.accesslist, subdomain.FromDomain) }} - {# All IPs not matched by accesslist will continue processing #} - {{ render_handles(subdomain_handles, subdomain.basicauth) }} - } - {% endif %} - {% endfor %} - {% set domain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('reverse', 'equalto', reverse['@uuid']) | selectattr('subdomain', 'undefined') | list %} {{ handle_accesslist(reverse.accesslist, reverse.FromDomain) }} - {# All IPs not matched by accesslist will continue processing #} - {{ render_handles(domain_handles, reverse.basicauth) }} + {{ render_basic_auth(reverse.basicauth) }} + {{ render_handles(domain_handles) }} } + + {% endif %} +{% endfor %} + +{% for subdomain in helpers.toList('Pischem.caddy.reverseproxy.subdomain') %} + {% if subdomain.enabled|default("0") == "1" %} + {% set reverse = helpers.toList('Pischem.caddy.reverseproxy.reverse') | selectattr('@uuid', 'equalto', subdomain.reverse) | first %} + {% if reverse and reverse.enabled|default("0") == "1" %} + {% if reverse.DisableTls|default("0") == "1" %}http://{% endif %}{{ subdomain.FromDomain|default("") }}{% if reverse.FromPort %}:{{ reverse.FromPort }}{% endif %} { + {% if reverse.AccessLog|default("0") == "1" %} + {% if generalSettings.LogAccessPlain|default("0") == "0" %} + log default + {% else %} + log { + output file /var/log/caddy/access/{{ subdomain['@uuid'] }}.log { + roll_keep_for {{ generalSettings.LogAccessPlainKeep|default("10") }}d + } + } + {% endif %} + {% endif %} + {{ tls_configuration( + clientAuthTrustPool=subdomain.ClientAuthTrustPool|default(""), + clientAuthMode=subdomain.ClientAuthMode|default("") + ) }} + {% set subdomain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('subdomain', 'equalto', subdomain['@uuid']) | list %} + {{ handle_accesslist(subdomain.accesslist, subdomain.FromDomain) }} + {{ render_basic_auth(subdomain.basicauth) }} + {{ render_handles(subdomain_handles) }} + } + + {% endif %} {% endif %} {% endfor %} diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeAuthProvider b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeAuthProvider index f179fc845..1c6beb34b 100644 --- a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeAuthProvider +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeAuthProvider @@ -7,16 +7,26 @@ {% set is_ipv6 = (':' in generalSettings.AuthToDomain and generalSettings.AuthToDomain.count(':') >= 2) %} {% set auth_url = (generalSettings.AuthToTls|default("0") == "1" and 'https://' or 'http://') + (is_ipv6 and '[' or '') + generalSettings.AuthToDomain|default("") + (is_ipv6 and ']' or '') + (generalSettings.AuthToPort and ':' + generalSettings.AuthToPort or '') %} {% endif %} +{% macro generate_copy_headers() %} + {% if generalSettings.CopyHeaders %} + {% for header_uuid in generalSettings.CopyHeaders.split(',') %} + {% set header = helpers.toList('Pischem.caddy.reverseproxy.header') | selectattr('@uuid', 'equalto', header_uuid) | first %} + {% if header and header.HeaderType %} + copy_headers {{ header.HeaderType }} + {% endif %} + {% endfor %} + {% endif %} +{% endmacro %} {% if generalSettings.AuthProvider == 'authelia' %} forward_auth {{ auth_url }} { {% if generalSettings.AuthToUri %} uri {{ generalSettings.AuthToUri|default("") }} {% endif %} - {% if generalSettings.AuthCopyHeaders|default("") == "" %} - copy_headers Remote-User Remote-Groups Remote-Name Remote-Email - {% else %} - copy_headers {{ generalSettings.AuthCopyHeaders.split(',') | join(' ') }} - {% endif %} + copy_headers Remote-User + copy_headers Remote-Groups + copy_headers Remote-Name + copy_headers Remote-Email + {{ generate_copy_headers() }} } {% elif generalSettings.AuthProvider == 'authentik' %} reverse_proxy /outpost.goauthentik.io/* {{ auth_url }} { @@ -28,10 +38,17 @@ {% if generalSettings.AuthToUri %} uri {{ generalSettings.AuthToUri|default("") }} {% endif %} - {% if generalSettings.AuthCopyHeaders|default("") == "" %} - copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version - {% else %} - copy_headers {{ generalSettings.AuthCopyHeaders.split(',') | join(' ') }} - {% endif %} + copy_headers X-Authentik-Username + copy_headers X-Authentik-Groups + copy_headers X-Authentik-Email + copy_headers X-Authentik-Name + copy_headers X-Authentik-Uid + copy_headers X-Authentik-Jwt + copy_headers X-Authentik-Meta-Jwks + copy_headers X-Authentik-Meta-Outpost + copy_headers X-Authentik-Meta-Provider + copy_headers X-Authentik-Meta-App + copy_headers X-Authentik-Meta-Version + {{ generate_copy_headers() }} } {% endif %} diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeDnsProvider b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeDnsProvider deleted file mode 100644 index e771a66b7..000000000 --- a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeDnsProvider +++ /dev/null @@ -1,193 +0,0 @@ -{# -# This file gets imported in two sections of the Caddyfile template -# - Section: Dynamic DNS Global Configuration -# - Macro: tls_configuration -# -# It only includes DNS Providers that need specific settings and do not default to -# "dns {{ dnsProvider }} {{ dnsApiKey }}" -#} -{% if dnsProvider == 'duckdns' %} - {% if dnsApiKey %}api_token {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}override_domain {{ dnsSecretApiKey }} - {% endif %} -{% elif dnsProvider == 'porkbun' %} - {% if dnsApiKey %}api_key {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}api_secret_key {{ dnsSecretApiKey }} - {% endif %} -{% elif dnsProvider == 'desec' %} - {% if dnsApiKey %}token {{ dnsApiKey }} - {% endif %} -{% elif dnsProvider == 'route53' %} - {% if dnsApiKey %}access_key_id {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}secret_access_key {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}hosted_zone_id {{ dnsOptionalField1 }} - {% endif %} - {% if dnsOptionalField2 %}profile {{ dnsOptionalField2 }} - {% endif %} - {% if dnsOptionalField3 %}region {{ dnsOptionalField3 }} - {% endif %} - {% if dnsOptionalField4 %}session_token {{ dnsOptionalField4 }} - {% endif %} -{% elif dnsProvider == 'acmedns' %} - {% if dnsApiKey %}username {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}subdomain {{ dnsOptionalField1 }} - {% endif %} - {% if dnsOptionalField2 %}server_url {{ dnsOptionalField2 }} - {% endif %} -{% elif dnsProvider == 'googleclouddns' %} - {% if dnsApiKey %}gcp_project {{ dnsApiKey }} - {% endif %} -{% elif dnsProvider == 'azure' %} - {% if dnsApiKey %}tenant_id {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}client_id {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}client_secret {{ dnsOptionalField1 }} - {% endif %} - {% if dnsOptionalField2 %}subscription_id {{ dnsOptionalField2 }} - {% endif %} - {% if dnsOptionalField3 %}resource_group_name {{ dnsOptionalField3 }} - {% endif %} -{% elif dnsProvider == 'ovh' %} - {% if dnsApiKey %}endpoint {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}application_key {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}application_secret {{ dnsOptionalField1 }} - {% endif %} - {% if dnsOptionalField2 %}consumer_key {{ dnsOptionalField2 }} - {% endif %} -{% elif dnsProvider == 'namecheap' %} - {% if dnsApiKey %}api_key {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}user {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}api_endpoint {{ dnsOptionalField1 }} - {% endif %} - {% if dnsOptionalField2 %}client_ip {{ dnsOptionalField2 }} - {% endif %} -{% elif dnsProvider == 'powerdns' %} - {% if dnsApiKey %}server_url {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}api_token {{ dnsSecretApiKey }} - {% endif %} -{% elif dnsProvider == 'ddnss' %} - {% if dnsApiKey %}api_token {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}username {{ dnsSecretApiKey }} - {% endif %} - password {{ dnsOptionalField1 }} -{% elif dnsProvider == 'linode' %} - {% if dnsApiKey %}api_token {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}api_url {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}api_version {{ dnsOptionalField1 }} - {% endif %} -{% elif dnsProvider == 'tencentcloud' %} - {% if dnsApiKey %}secret_id {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}secret_key {{ dnsSecretApiKey }} - {% endif %} -{% elif dnsProvider == 'dinahosting' %} - {% if dnsApiKey %}username {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} - {% endif %} -{% elif dnsProvider == 'hexonet' %} - {% if dnsApiKey %}username {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} - {% endif %} -{% elif dnsProvider == 'mailinabox' %} - {% if dnsApiKey %}api_url {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}email_address {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}password {{ dnsOptionalField1 }} - {% endif %} -{% elif dnsProvider == 'netcup' %} - {% if dnsApiKey %}customer_number {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}api_key {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}api_password {{ dnsOptionalField1 }} - {% endif %} -{% elif dnsProvider == 'rfc2136' %} - {% if dnsApiKey %}key_name {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}key_alg {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}key {{ dnsOptionalField1 }} - {% endif %} - {% if dnsOptionalField2 %}server {{ dnsOptionalField2 }} - {% endif %} -{% elif dnsProvider == 'dnsmadeeasy' %} - {% if dnsApiKey %}api_key {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}secret_key {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}api_endpoint {{ dnsOptionalField1 }} - {% endif %} -{% elif dnsProvider == 'civo' %} - {% if dnsApiKey %}api_token {{ dnsApiKey }} - {% endif %} -{% elif dnsProvider == 'scaleway' %} - {% if dnsApiKey %}secret_key {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}organization_id {{ dnsSecretApiKey }} - {% endif %} -{% elif dnsProvider == 'acmeproxy' %} - {% if dnsApiKey %}username {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}endpoint {{ dnsOptionalField1 }} - {% endif %} -{% elif dnsProvider == 'inwx' %} - {% if dnsApiKey %}username {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}shared_secret {{ dnsOptionalField1 }} - {% endif %} - {% if dnsOptionalField2 %}endpoint_url {{ dnsOptionalField2 }} - {% endif %} -{% elif dnsProvider == 'namedotcom' %} - {% if dnsApiKey %}token {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}server {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}user {{ dnsOptionalField1 }} - {% endif %} -{% elif dnsProvider == 'easydns' %} - {% if dnsApiKey %}api_token {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}api_key {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}api_url {{ dnsOptionalField1 }} - {% endif %} -{% elif dnsProvider == 'directadmin' %} - {% if dnsApiKey %}host {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}user {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}login_key {{ dnsOptionalField1 }} - {% endif %} - {% if dnsOptionalField2 %}insecure_requests {{ dnsOptionalField2 }} - {% endif %} -{% elif dnsProvider == 'cloudns' %} - {% if dnsApiKey %}auth_id {{ dnsApiKey }} - {% endif %} - {% if dnsSecretApiKey %}auth_password {{ dnsSecretApiKey }} - {% endif %} - {% if dnsOptionalField1 %}sub_auth_id {{ dnsOptionalField1 }} - {% endif %} -{% endif %} diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 index 92d638759..53b04b1a9 100644 --- a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 @@ -99,7 +99,7 @@ {% if layer4.FromOpenvpnStaticKey %} group_key_direction {{ direction }} {% for key_uuid in key_list %} - group_key_file /var/db/caddy/data/caddy/certificates/temp/{{ key_uuid.strip() }}.key + group_key_file /usr/local/etc/caddy/certificates/{{ key_uuid.strip() }}.key {% endfor %} {% endif %} } @@ -111,7 +111,7 @@ modes crypt {% if layer4.FromOpenvpnStaticKey %} {% for key_uuid in key_list %} - group_key_file /var/db/caddy/data/caddy/certificates/temp/{{ key_uuid.strip() }}.key + group_key_file /usr/local/etc/caddy/certificates/{{ key_uuid.strip() }}.key {% endfor %} {% endif %} } @@ -121,7 +121,7 @@ modes crypt2 {% if layer4.FromOpenvpnStaticKey %} {% for key_uuid in key_list %} - client_key_file /var/db/caddy/data/caddy/certificates/temp/{{ key_uuid.strip() }}.key + client_key_file /usr/local/etc/caddy/certificates/{{ key_uuid.strip() }}.key {% endfor %} {% endif %} } @@ -133,7 +133,7 @@ modes crypt2 {% if layer4.FromOpenvpnStaticKey %} {% for key_uuid in key_list %} - server_key_file /var/db/caddy/data/caddy/certificates/temp/{{ key_uuid.strip() }}.key + server_key_file /usr/local/etc/caddy/certificates/{{ key_uuid.strip() }}.key {% endfor %} {% endif %} } diff --git a/www/caddy/src/opnsense/www/js/widgets/CaddyCertificate.js b/www/caddy/src/opnsense/www/js/widgets/CaddyCertificate.js deleted file mode 100644 index 7a3834294..000000000 --- a/www/caddy/src/opnsense/www/js/widgets/CaddyCertificate.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2024 Cedrik Pischem - * 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. - */ - -export default class CaddyCertificate extends BaseTableWidget { - constructor() { - super(); - this.tickTimeout = 30; - } - - getGridOptions() { - return { - // trigger overflow-y:scroll after 650px height - sizeToContent: 650 - }; - } - - getMarkup() { - const $container = $('
        '); - const $caddyCertificateTable = this.createTable('caddyCertificateTable', { - headerPosition: 'none' - }); - - $container.append($caddyCertificateTable); - return $container; - } - - async onWidgetTick() { - const proxyData = await this.ajaxCall('/api/caddy/reverse_proxy/get'); - if (!proxyData.caddy.general || proxyData.caddy.general.enabled === "0") { - this.displayError(`${this.translations.unconfigured}`); - return; - } - - const domains = Object.values(proxyData.caddy.reverseproxy?.reverse || []) - .map(proxy => proxy.FromDomain) - .filter(Boolean); - - const certificates = (await this.ajaxCall('/api/caddy/diagnostics/certificate')).content || []; - - // Display certificate if hostname in config and CN of stored cert on disk match - const matchingCertificates = certificates.filter(cert => domains.includes(cert.hostname)); - - if (matchingCertificates.length === 0) { - this.displayError(`${this.translations.nocerts}`); - return; - } - - this.clearError(); - this.processCertificates(matchingCertificates); - } - - displayError(message) { - const $error = $(``); - $('#caddyCertificateTable').empty().append($error); - } - - clearError() { - $('#caddyCertificateTable .error-message').remove(); - } - - processCertificates(certificates) { - $('.caddy-certificate-tooltip').tooltip('hide'); - - const rows = certificates.map(certificate => { - const colorClass = certificate.remaining_days === 0 - ? 'text-danger' - : certificate.remaining_days < 14 - ? 'text-warning' - : 'text-success'; - - const statusText = certificate.remaining_days === 0 - ? this.translations.expired - : this.translations.valid; - - const row = ` -
        - - -   - ${certificate.hostname} -
        -
        - ${this.translations.expires} ${certificate.remaining_days} ${this.translations.days}, - ${new Date(certificate.expiration_date).toLocaleString()} -
        -
        `; - return { html: row, expirationDate: new Date(certificate.expiration_date) }; - }); - - rows.sort((a, b) => a.expirationDate - b.expirationDate); - - const sortedRows = rows.map(row => [row.html]); - super.updateTable('caddyCertificateTable', sortedRows); - - $('.caddy-certificate-tooltip').tooltip({ container: 'body' }); - } -} diff --git a/www/caddy/src/opnsense/www/js/widgets/CaddyDomain.js b/www/caddy/src/opnsense/www/js/widgets/CaddyDomain.js index ecfbf0f24..63fe9ee75 100644 --- a/www/caddy/src/opnsense/www/js/widgets/CaddyDomain.js +++ b/www/caddy/src/opnsense/www/js/widgets/CaddyDomain.js @@ -48,7 +48,7 @@ export default class CaddyDomain extends BaseTableWidget { async onWidgetTick() { // Check if caddy is enabled - const data = await this.ajaxCall('/api/caddy/reverse_proxy/get'); + const data = await this.ajaxCall(`/api/caddy/reverse_proxy/${'get'}`); if (!data.caddy.general || data.caddy.general.enabled === "0") { this.displayError(`${this.translations.unconfigured}`); return; diff --git a/www/caddy/src/opnsense/www/js/widgets/Metadata/Caddy.xml b/www/caddy/src/opnsense/www/js/widgets/Metadata/Caddy.xml index c423f6163..230bb1aee 100644 --- a/www/caddy/src/opnsense/www/js/widgets/Metadata/Caddy.xml +++ b/www/caddy/src/opnsense/www/js/widgets/Metadata/Caddy.xml @@ -3,7 +3,7 @@ CaddyDomain.js /ui/caddy/reverse_proxy - /api/caddy/reverse_proxy/get + /api/caddy/reverse_proxy/* Caddy Domains @@ -13,21 +13,4 @@ Caddy does not manage any domains. - - CaddyCertificate.js - /ui/caddy/reverse_proxy - - /api/caddy/diagnostics/certificate - /api/caddy/reverse_proxy/get - - - Caddy Certificates - Valid - Expired - Caddy is disabled or not configured. - Caddy does not manage any automatic certificates. - Expires: - days - - diff --git a/www/nginx/Makefile b/www/nginx/Makefile index 0b7db7007..59e1f9511 100644 --- a/www/nginx/Makefile +++ b/www/nginx/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= nginx -PLUGIN_VERSION= 1.34 -PLUGIN_REVISION= 6 +PLUGIN_VERSION= 1.35 +PLUGIN_REVISION= 3 PLUGIN_COMMENT= Nginx HTTP server and reverse proxy PLUGIN_DEPENDS= nginx PLUGIN_MAINTAINER= franz.fabian.94@gmail.com diff --git a/www/nginx/pkg-descr b/www/nginx/pkg-descr index 464215e46..c12a09328 100644 --- a/www/nginx/pkg-descr +++ b/www/nginx/pkg-descr @@ -5,11 +5,21 @@ NGINX functionality includes HTTP server, HTTP and mail reverse proxy, caching, load balancing, compression, request throttling, connection multiplexing and reuse, SSL offload and HTTP media streaming. -WWW: https://nginx.org/ - Plugin Changelog ================ +1.35 + +* Global options sendfile directive typo fix +* Add HTTP/2 option to GUI +* Add multiple client authentication trusted CA support +* Add proxy_intercept_errors directive support +* Add Variables hashes size support +* Add proxy_cache_valid directive support with response codes and multiple selection options +* Clean up model definition file +* Change ban_ttl default value to avoid unintentional system slowdown +* Fix NAXSI rules install + 1.34 * Add the option to not log TLS handshakes @@ -17,6 +27,7 @@ Plugin Changelog * Migrate from the deprecated 'listen … http2' directive to the 'http2' directive * Limit CSP log file size (migration notice: if you want to keep CSP violations logged, you will need to enable logging in the security policy) * Add basic support for forced caching (contributed by Eirik Øverby) +* Add ability to override SNI for streams (contributed by DodoLeDev) 1.33 @@ -154,8 +165,8 @@ Plugin Changelog 1.14 * add load balancer algorithm option (ip_hash) -* fix URL flag not stored (contributed by jkellerer) [1] -* allow to whitelist specific source IPs in the web application firewall (contributed by Julio Cesar Camargo) [2] +* fix URL flag not stored (contributed by jkellerer)[1] +* allow to whitelist specific source IPs in the web application firewall (contributed by Julio Cesar Camargo)[2] [1] https://github.com/opnsense/plugins/pull/1375 [2] https://github.com/opnsense/plugins/pull/1310 @@ -218,7 +229,7 @@ Plugin Changelog * Add proxy options for ignore client abort and disabling buffering * Add logviewer support for streams -* Fix charset is not defined bug (contributed by ccesario [1]) +* Fix charset is not defined bug (contributed by ccesario)[1] * Add an existence check for locations * Add PROXY protocol for HTTP and Streams frontend * Add PROXY backend support for Streams @@ -229,14 +240,14 @@ Plugin Changelog 1.4 (Development only) * move upstreams from HTTP to their own menu because they are used for TCP load balancing as well -* add TCP load balancing [1] +* add TCP load balancing[1] * add support for IP based ACLs -* add log rotation (contributed by Julius Cesar Camargo [2]) +* add log rotation (contributed by Julius Cesar Camargo)[2] * add support for satisfy, body size limitation -* change: allow to disable internal bot protection (contributed by @fzoske) [3] +* change: allow to disable internal bot protection (contributed by fzoske)[3] * change: do not save when no change in the list happened to prevent filling the log history * fix: translate a german string in upstream server to english -* replace headers instead of just adding our own (duplication issue #971), suppress X-Powered-By from Upstream [4] +* replace headers instead of just adding our own (duplication issue #971), suppress X-Powered-By from Upstream[4] [1] https://github.com/opnsense/plugins/pull/930 [2] https://github.com/opnsense/plugins/pull/982 @@ -252,13 +263,13 @@ Plugin Changelog * add limiter support * add permanent ban feature (use alias in firewall rules) * add caching feature (local file system) -* add path prefix support (contributed by @ccesario ) +* add path prefix support (contributed by ccesario) * add port to upstream server table * add support to download waf rules * improvement: export full chain instead of only the server certificate (fixes some issues with some tools which need it) * web interface (allow HTTP/2 server push to increase performance) * support for (secure) web sockets in reverse proxy mode -* bugfix: fix issues with multiple hostnames (IPv6 issues with TLS SNI by @ccesario ) +* bugfix: fix issues with multiple hostnames due to IPv6 issues with TLS SNI (contributed by ccesario) * bugfix: warning in certificate rendering 1.1 diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php index 0458bc2aa..7b2043b90 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php @@ -249,6 +249,32 @@ class SettingsController extends ApiMutableModelControllerBase return $this->setBase('resolver', 'resolver', $uuid); } + // proxy_cache_valid + public function searchproxyCacheValidAction() + { + return $this->searchBase('proxy_cache_valid', array('uuid', 'description', 'code', 'valid')); + } + + public function getproxyCacheValidAction($uuid = null) + { + return $this->getBase('proxy_cache_valid', 'proxy_cache_valid', $uuid); + } + + public function addproxyCacheValidAction() + { + return $this->addBase('proxy_cache_valid', 'proxy_cache_valid'); + } + + public function delproxyCacheValidAction($uuid) + { + return $this->delBase('proxy_cache_valid', $uuid); + } + + public function setproxyCacheValidAction($uuid) + { + return $this->setBase('proxy_cache_valid', 'proxy_cache_valid', $uuid); + } + // http server public function searchhttpserverAction() { diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php index a30d036d1..01ad3330a 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php @@ -64,6 +64,7 @@ class IndexController extends \OPNsense\Base\IndexController $this->view->errorpage = $this->getForm("errorpage"); $this->view->tls_fingerprint = $this->getForm("tls_fingerprint"); $this->view->resolver = $this->getForm("resolver"); + $this->view->proxy_cache_valid = $this->getForm("proxy_cache_valid"); $this->view->syslog_target = $this->getForm("syslog_target"); $nginx = new Nginx(); $this->view->show_naxsi_download_button = diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml index 33cf1ce52..21f8b4a28 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml @@ -113,7 +113,8 @@ httpserver.ca - dropdown + select_multiple + Trusted CA certificates httpserver.verify_client @@ -165,6 +166,13 @@ checkbox If the request scheme is not HTTPS, redirect to use HTTPS for this server. + + httpserver.http2 + + checkbox + Enable the HTTP/2 protocol. + true + httpserver.tls_protocols @@ -320,4 +328,11 @@ select_multiple Select custom error pages to display instead of the default builtin error pages. If at least one error page is selected here, all default error pages will be disabled. + + httpserver.proxy_intercept_errors + + checkbox + Intercept responses with codes greater than or equal to 300 and redirect to processing with custom error pages. + true + diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml index d6ee67abb..23fdccff5 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml @@ -98,11 +98,19 @@ location.cache_valid - + text true Force caching of 200, 301 and 302 responses according to the request methods enabled for caching. Given in minutes; leave empty to rely on request/response headers from client and upstream. + + location.proxy_cache_valid + + select_multiple + + true + Force caching of response codes specified at Response Code Caching page. + location.cache_background_update @@ -335,4 +343,12 @@ select_multiple Select custom error pages to display instead of the default builtin error pages. Selection will override error pages configured on HTTP server. + + location.proxy_intercept_errors + + dropdown + + Intercept responses with codes greater than or equal to 300 and redirect to processing with custom error pages. + true + diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/proxy_cache_valid.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/proxy_cache_valid.xml new file mode 100644 index 000000000..e37702e9d --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/proxy_cache_valid.xml @@ -0,0 +1,22 @@ +
        + + proxy_cache_valid.description + + text + Brief description for reference. + + + proxy_cache_valid.code + + true + + select_multiple + Enter a Respone codes or use "any" to cache any responses. + + + proxy_cache_valid.valid + + text + Specify caching time in minutes. + +
        diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml index 8ac01921c..1fc2c9c9c 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml @@ -11,7 +11,7 @@ nginx.general.ban_ttl text - Set autoblock lifetime in minutes. Set to 0 for infinite. + Set autoblock lifetime in minutes. 72 hours by default. Set to 0 for infinite. Please note that setting this to 0 may result in gradual system slowdown and the need to manually clear the entries. true @@ -31,7 +31,7 @@ true - nginx.http.enabled + nginx.http.sendfile checkbox Enable sendfile support (faster). @@ -56,7 +56,7 @@ nginx.http.server_names_hash_bucket_size - + text true @@ -66,6 +66,18 @@ text true + + nginx.http.variables_hash_bucket_size + + text + true + + + nginx.http.variables_hash_max_size + + text + true + nginx.http.bots_ua diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_35_1.php b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_35_1.php new file mode 100644 index 000000000..8f545c7fb --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_35_1.php @@ -0,0 +1,46 @@ + + * 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. + */ + +namespace OPNsense\Nginx\Migrations; + +use OPNsense\Base\BaseModelMigration; + +class M1_35_1 extends BaseModelMigration +{ + // Rewrite default ban_ttl value + public function run($model) + { + $general_node = $model->getNodeByReference('general'); + + if ($general_node->ban_ttl->isEqual('0')) { + $general_node->ban_ttl = '4320'; + } + // run default migration actions + parent::run($model); + } +} diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml index 22f15d3ff..88d65ee88 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml @@ -1,1199 +1,1087 @@ - //OPNsense/Nginx - 1.34 - nginx web server, reverse proxy and waf - - - - 0 - Y - - - 0 - 0 - Y - - - - - - 0 - Y - - - - - - 1 - 1 - Y - - - 1024 - 1 - Y - - - 0 - N - - - 60 - N - - - 0 - Y - - - N - - - N - 1 - - - N - 1 - - - N - - 403 Forbidden - 444 Terminate Connection - - Y - 403 - - - 0 - Y - - - Y - Python-urllib,Nmap,python-requests,libwww-perl,MJ12bot,Jorgee,fasthttp,libwww,Telesphoreo,A6-Indexer,ltx71,okhttp,ZmEu,sqlmap,LMAO/2.0,l9explore,l9tcpid,Masscan,zgrab,Ronin/2.0,Hakai/2.0,Indy\sLibrary,^Mozilla/[\d\.]+$,Morfeus\sFucking\sScanner,MSIE\s[0-6]\.\d+ - - - N - - - - - - Y - - - - - - Selected user not found - Y - Y - - - - - - Y - - - Y - - - - - - Y - - - - - - Selected server not found - Y - Y - - - - IP Hash - - Weighted Round Robin - N - - - 0 - N - - - 1 - N - - - 1 - N - - - 1 - N - - - 0 - Y - - - 0 - Y - - - Y - 0 - - - Y - 0 - - - cert - N - - - N - - - Y - - TLSv1 - TLSv1.1 - TLSv1.2 - TLSv1.3 - - N - - - Y - 1 - - - ca - Y - N - - - Y - 1 - - - N - 1 - 1 - - - - - - Y - - - Y - - - Y - - - 0 - Y - - - N - - - N - - - N - - - - Permanently Unreachable - Backup Server - - N - - - - - - Y - - - Y - - - - Exact Match ("=") - Case Sensitive Match ("~") - Case Insensitive Match ("~*") - Don't check regular expressions on longest prefix match ("^~") - - N - - - 0 - Y - - - 0 - Y - - - - - - Selected error page(s) not found - N - N - - - N - - - N - - - - - - Selected server not found - N - Y - - - - - - Selected upstream not found - N - N - - - N - /^[^" \t]+$/ - - - - - - Selected cache directory not found - N - N - - - Y - - Error - Timeout - Invalid_header - Updating - HTTP Status Code 403 - HTTP Status Code 404 - HTTP Status Code 429 - HTTP Status Code 500 - HTTP Status Code 502 - HTTP Status Code 503 - HTTP Status Code 504 - - N - - - Y - - Post - - - N - - - Y - 1 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - - - - Selected rewrite(s) not found - N - Y - - - N - - - N - - - N - - - - - - Selected user file not found - N - N - - - 0 - Y - - - N - - - Y - 0 - - - N - - - - - - Selected limit zone not found - N - Y - - - N - /^\d+[kmg]$/i - Enter a number followed by k, m or g. - - - N - /^\d+[kmg]$/i - Enter a number followed by k, m or g. - - - Y - 0 - - - Y - 0 - - - WebSocket and Upstream Keepalive support can not be combined. - SingleSelectConstraint - - upstream_keepalive - - - - - - Y - 0 - - - websocket.check001 - - - - - N - 1 - - - NgxBusyBufferConstraint - - - - - N - 1 - - - NgxBusyBufferConstraint - - - - - N - 1 - - - NgxBusyBufferConstraint - - - - - N - 1 - - - NgxBusyBufferConstraint - - - - - Y - 0 - - - Y - 1 - - - Y - 1 - - - N - 1 - - - N - 1 - - - - - - Selected ACL not found - N - N - - - - Any - All - - N - - - N - 0 - - - Y - 0 - - - - - - Selected error page(s) not found - N - Y - - - - - - Y - - - - - - Selected rule not found - Y - Y - - - Y - - - Y - >= - - Bigger or Equal - Bigger - Lesser - Lesser or Equal - - - - Y - BLOCK - - Block Request - Allow Request - Drop The Connection - Log Request - - - - - - - Y - - - -
        Main Rule
        - Basic Rule -
        - Y -
        - - N - /^[^"]+$/ - - - This field must be set. - SetIfConstraint - match_type - id - - - - - Y - - - NaxsiIdentifierConstraint - - - - - N - /^[^"]+$/ - - - N - /^[^"]+$/ - - - This field must be set. - SetIfConstraint - match_type - id - - - - - Y - id - - Blacklist - Whitelist - - - - identifier.check001 - - - - - Y - - - N - 8 - - - This field must be set. - SetIfConstraint - match_type - id - - - - - Y - - - Y - - - 0 - Y - - - Y - - - 0 - Y - - - /^[^"]+$/ - - - /^[^"]+$/ - - - /^[^"]+$/ - - - Y - - - Y - - - Y - -
        - - - - Y - - - - - - Selected SYSLOG target not found - N - Y - - - N - Y - 80,[::]:80 - /^(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+)(?:\s*,\s*(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+))*$/i - Please provide a valid listen address or port, i.e. 127.0.0.1:8080, [::1]:8080, 8080. - - - NgxUniqueDefaultServerConstraint - - - - - N - Y - 443,[::]:443 - /^(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+)(?:\s*,\s*(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+))*$/i - Please provide a valid listen address or port, i.e. 127.0.0.1:8080, [::1]:8080, 8080. - - - Y - 0 - - - NgxUniqueDefaultServerConstraint - - - - - 0 - Y - - - 0 - Y - - - N - /^((?:\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?(,?(?:(?:(\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?))*$/i - Y - - - - - - Selected alias not found - N - N - - - - X-Real-IP (default) - X-Forwarded-For - PROXY Protocol - CloudFlare Connecting IP - - N - - - - - - Selected location(s) not found - N - Y - - - - - - Selected rewrite(s) not found - N - Y - - - N - - - cert - N - - - ca - N - - - Off - - Off - On - Optional - Optional, don't verify - - Y - - - main - -
        Default
        - Extended - Anonymized - Disabled -
        - Y -
        - - N - - Emergency - Alert - Critical - Error (default) - Warning - Notice - Informational - - Y - error - - - 1 - Y - - - Y - 1 - - - utf-8 - - utf-8 - - N - - - 0 - Y - - - Y - Y - - TLSv1.2 - TLSv1.3 - - Y - TLSv1.2,TLSv1.3 - - - ECDHE-ECDSA-CAMELLIA256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CAMELLIA256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CAMELLIA128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CAMELLIA128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-CAMELLIA256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-CAMELLIA256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-CAMELLIA128-SHA256:ECDHE-RSA-AES128-SHA256 - N - /^((((!|\+|-)?[A-Z][A-Z\d\+-]+)|(@STRENGTH)):?)*$/i - - - N - /^(([A-Z\d-]+):?)*$/i - - - 1 - Y - - - - - - Selected resolver not found - N - N - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - N - /^((?:\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?(,?(?:(?:(\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?))*$/i - Y - - - 0 - Y - - - 1 - Y - - - Y - 1 - 1 - - - Y - 4 - 1 - - - Y - 8 - 1 - - - - - - Selected security rule not found - N - N - - - - - - Selected limit zone not found - N - Y - - - N - /^\d+[kmg]$/i - Enter a number followed by k, m or g. - - - N - /^\d+[kmg]$/i - Enter a number followed by k, m or g. - - - - - - Selected ACL not found - N - N - - - N - N - Local Database - - - - Any - All - - N - - - Y - 0 - - - - - - Selected error page(s) not found - N - Y - -
        - - - - N - Y - /^(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+)(?:\s*,\s*(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+))*$/i - Please provide a valid listen address or port, i.e. 127.0.0.1:8080, [::1]:8080, 8080. - - - - - - Selected SYSLOG target not found - N - Y - - - Y - 0 - - - N - /^((?:\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?(,?(?:(?:(\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?))*$/i - Y - - - 0 - Y - - - cert - N - - - ca - N - - - Off - - Off - On - Optional - Optional, don't verify - - Y - - - main - -
        Default
        - Extended - Anonymized - Disabled -
        - Y -
        - - N - - Emergency - Alert - Critical - Error - Warning - Notice - Informational (default) - - Y - info - - - upstream - - Upstream - SNI Upstream Mapping - - Y - - - - - - Selected upstream not found - N - N - - - This field must be set. - SetIfConstraint - route_field - upstream - - - - - - - - Selected upstream not found - N - N - - - This field must be set. - SetIfConstraint - route_field - sni_upstream_map - - - - - - - - Selected ACL not found - N - N - - - N - 0 - - - N - 0 - - - N - 0 - -
        - - - - Y - - - - Y - - - - - - Y - - - - - - Y - N - - - - - - Y - - - - Y - - - - Deny Access - Allow Access - - N - - - - - - Y - - - deny - - Deny Access - Allow Access - - Y - - - - - - Y - /^[^" \t]+$/i - -
        - Y - Y - /^(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\]))(:\d+|:?\d+)*)(?:\s*,\s*(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\]))(:\d+|:?\d+)*))*$/i - Please provide a valid resolver address, i.e. 8.8.8.8, [2001:4860:4860::8888], 8.8.8.8:5353. -
        - - 1 - N - - - N - - - Can not disable all record types. - SingleSelectConstraint - - ipv6_off - - - - - - N - - - ipv4_off.check001 - - - - - 1 - N - -
        - - - - Y - /^[^" \t]+$/i - - - Y - - - Y - /^[^" \t]+$/i - - - - Stop processing rules - Stop processing rules and find location - Redirect - Permanent - - N - - - - - - Y - - - N - - No Referrer - No Referrer When Downgrading - Same Origin (recommended) - Origin - Strict Origin - Strict Origin When Cross Origin - Origin When Cross Origin - Unsafe URL - - N - - - N - - Block - Off - On - - N - - - Y - - - N - - - Y - 1 - - - Y - 0 - - - Y - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - - - - Y - - - binary_remote_addr - - Remote IP Address - - Y - - - r/s - - Requests Per Second - Requests Per Minute - - Y - - - Y - 10 - 1 - - - Y - 20 - 1 - - - - - - Y - - - Y - - 400 Bad Request - 401 Unauthorized - 403 Forbidden - 404 Not Found - 405 Method Not Allowed - 407 Proxy Authentication Required - 408 Request Timeout - 410 Gone - 415 Unsupported Media Type - 429 Too Many Requests - 431 Request Header Fields Too Large - 500 Internal Server Error - 501 Not Implemented - 502 Bad Gateway - 503 Service Unavailable - 504 Gateway Timeout - - Y - - - PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04Ij4KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsaW5pdGlhbC1zY2FsZT0xIiAvPgogICAgPHRpdGxlPkVycm9yPC90aXRsZT4KPC9oZWFkPgo8Ym9keT4KICAgIDxoMT5FcnJvcjwvaDE+CiAgICA8cD5Tb3JyeSwgYnV0IHNvbWV0aGluZyB3ZW50IHdyb25nLjwvcD4KPC9ib2R5Pgo8L2h0bWw+ - - - SingleSelectConstraint - Page content is required if redirect is not used and needs to be empty otherwise. - - redirect - - Y - - - - - N - - - pagecontent.check001 - - - - - N - - 200 OK - 300 Multiple Choice - 301 Moved Permanently - 302 Found - 401 Unauthorized - 403 Forbidden - 404 Not Found - 451 Unavailable For Legal Reasons - 500 Internal Server Error - 503 Service Unavailable - - N - - - - - - Y - - - Y - - - 0 - Y - - - - N - /^(0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)(?::((0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)))*$/ - - - - Y - /^(0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)(?::((0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)))*$/ - - - - - - - - - Selected limit zone not found - Y - Y - - - 1 - Y - 5 - - - 1 - N - 20 - - - 1 - Y - - - Y - - - - - - Y - - - - - - - Y - /\/(srv|var|tmp|mnt)[a-z0-9\-\._\:\,\/]+[a-z0-9\-\._\:\,]+/i - - - 10 - 10 - - - N - 1 - - - Y - 0 - - - N - 1 - - - - - - Y - - - Y - - - N - - - - kern - user - mail - daemon - auth - intern - lpr - news - uucp - clock - authpriv - ftp - ntp - audit - alert - cron - local0 - local1 - local2 - local3 - local4 - local5 - local6 - local7 - - Y - local7 - - - - debug - info - notice - warn - error - crit - alert - emerg - - Y - error - - - N - /[a-z][a-z0-9]*/i - - - Y - N - - -
        + Y + + + + Deny Access + Allow Access + + + + + + Y + + + deny + + Deny Access + Allow Access + + Y + + + + + Y + /^[^" \t]+$/i + +
        + Y + Y + /^(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\]))(:\d+|:?\d+)*)(?:\s*,\s*(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\]))(:\d+|:?\d+)*))*$/i + Please provide a valid resolver address, i.e. 8.8.8.8, [2001:4860:4860::8888], 8.8.8.8:5353. +
        + + 1 + + + + + Can not disable all record types. + SingleSelectConstraint + + ipv6_off + + + + + + + + ipv4_off.check001 + + + + + 1 + +
        + + + Y + /^[^" \t]+$/i + + + Y + + + Y + /^[^" \t]+$/i + + + + Stop processing rules + Stop processing rules and find location + Redirect + Permanent + + + + + + Y + + + + No Referrer + No Referrer When Downgrading + Same Origin (recommended) + Origin + Strict Origin + Strict Origin When Cross Origin + Origin When Cross Origin + Unsafe URL + + + + + Block + Off + On + + + + Y + + + + Y + 1 + + + Y + 0 + + + Y + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + + Y + + + binary_remote_addr + + Remote IP Address + + Y + + + r/s + + Requests Per Second + Requests Per Minute + + Y + + + Y + 10 + 1 + + + Y + 20 + 1 + + + + + Y + + + Y + + 400 Bad Request + 401 Unauthorized + 403 Forbidden + 404 Not Found + 405 Method Not Allowed + 407 Proxy Authentication Required + 408 Request Timeout + 410 Gone + 415 Unsupported Media Type + 429 Too Many Requests + 431 Request Header Fields Too Large + 500 Internal Server Error + 501 Not Implemented + 502 Bad Gateway + 503 Service Unavailable + 504 Gateway Timeout + + Y + + + + + SingleSelectConstraint + Page content is required if redirect is not used and needs to be empty otherwise. + + redirect + + Y + + + + + + + pagecontent.check001 + + + + + + 200 OK + 300 Multiple Choice + 301 Moved Permanently + 302 Found + 401 Unauthorized + 403 Forbidden + 404 Not Found + 451 Unavailable For Legal Reasons + 500 Internal Server Error + 503 Service Unavailable + + + + + + Y + + + Y + + + 0 + Y + + + + /^(0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)(?::((0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)))*$/ + + + + Y + /^(0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)(?::((0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)))*$/ + + + + + + + + Selected limit zone not found. + Y + Y + + + 1 + Y + 5 + + + 1 + + + 1 + Y + + + Y + + + + + Y + + + + + + Y + /\/(srv|var|tmp|mnt)[a-z0-9\-\._\:\,\/]+[a-z0-9\-\._\:\,]+/i + + + 10 + + + 1 + + + Y + 0 + + + 1 + + + + + Y + /^[^" \t]+$/i + + + Y + any + Y + /(^\d{3}(,\d{3})*$)|(^any$)/ + Please use three digit response code(s) or use "any" word. + + + 1 + Y + + + + + Y + + + Y + + + + + kern + user + mail + daemon + auth + intern + lpr + news + uucp + clock + authpriv + ftp + ntp + audit + alert + cron + local0 + local1 + local2 + local3 + local4 + local5 + local6 + local7 + + Y + local7 + + + + debug + info + notice + warn + error + crit + alert + emerg + + Y + error + + + /[a-z][a-z0-9]*/i + + + Y + N + + +
        diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt index 5c715e748..f64990afb 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt @@ -193,6 +193,9 @@
      1. {{ lang._('Cache Path')}}
      2. +
      3. + {{ lang._('Response Code Caching')}} +
      4. {{ lang._('Error Pages')}}
      5. @@ -603,6 +606,30 @@
        +
        + + + + + + + + + + + + + + + + + + +
        {{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('Codes') }}{{ lang._('Time') }}{{ lang._('Commands') }}
        + + +
        +
        @@ -825,6 +852,7 @@ {{ partial("layout_partials/base_dialog",['fields': limit_request_connection,'id':'limit_request_connectiondlg', 'label':lang._('Edit Request Connection Limit')]) }} {{ partial("layout_partials/base_dialog",['fields': limit_zone,'id':'limit_zonedlg', 'label':lang._('Edit Limit Zone')]) }} {{ partial("layout_partials/base_dialog",['fields': cache_path,'id':'cache_pathdlg', 'label':lang._('Edit Cache Path')]) }} +{{ partial("layout_partials/base_dialog",['fields': proxy_cache_valid,'id':'proxy_cache_validdlg', 'label':lang._('Edit Response Code Caching')]) }} {{ partial("layout_partials/base_dialog",['fields': sni_hostname_map,'id':'sni_hostname_mapdlg', 'label':lang._('Edit SNI Hostname Mapping')]) }} {{ partial("layout_partials/base_dialog",['fields': ipacl,'id':'ipacl_dlg', 'label':lang._('Edit IP ACL')]) }} {{ partial("layout_partials/base_dialog",['fields': errorpage,'id':'errorpage_dlg', 'label':lang._('Edit Error Page')]) }} diff --git a/www/nginx/src/opnsense/scripts/nginx/naxsi_rule_download.php b/www/nginx/src/opnsense/scripts/nginx/naxsi_rule_download.php index 0ef130059..00a8e7aec 100755 --- a/www/nginx/src/opnsense/scripts/nginx/naxsi_rule_download.php +++ b/www/nginx/src/opnsense/scripts/nginx/naxsi_rule_download.php @@ -81,7 +81,7 @@ function parse_rules($data) } $tmp = trim($matches[1]); $parsed[$tmp] = []; - } elseif (preg_match('/\S+ "(str|rx):([^\"]+)" "msg:([^\\"]*)" "mz:([^\"]*)" "s:([^\"]*):(\d+)" id:(\d+);/', $line, $matches)) { + } elseif (preg_match('/\S+ "(str|rx):(.+)" "msg:([^\\"]*)" +"mz:([^\"]*)" "s:([^\"]*):(\d+)" id:(\d+);/', $line, $matches)) { $parsed[$tmp][] = prepare_values(array_combine($description, $matches)); } } @@ -167,17 +167,10 @@ function save_to_model($data) } $policy->naxsi_rules = implode(',', array_diff($rule_list, $dis_rules)); } - $val_result = $model->performValidation(false); - if (count($val_result) !== 0) { - print_r($val_result); - exit(1); - } - - $model->serializeToConfig(); + // skip validation on serialization. possible warnings and errors will still end up in the syslog + $model->serializeToConfig(false, true); Config::getInstance()->save(); } - -#$data = parse_rules(file('./naxsi_core.rules')); $data = parse_rules(explode("\n", download_rules())); save_to_model($data); diff --git a/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php b/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php index d43ea8f98..93bfab68b 100755 --- a/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php +++ b/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php @@ -76,7 +76,7 @@ function modify_blocklist($tablename, array $allIps, $operation = "add"): void foreach (array_chunk($allIps, $chunkSize) as $ips) { $escapedIps = join(" ", array_map("escapeshellarg", $ips)); - exec_hidden("/sbin/pfctl -t ${tablename} -T ${operation} ${escapedIps}"); + exec_hidden("/sbin/pfctl -t {$tablename} -T {$operation} {$escapedIps}"); } } @@ -89,7 +89,7 @@ function read_all_from_blocklist($tablename) 2 => ['file', "/dev/null", "w"], ]; - $process = proc_open("/sbin/pfctl -t ${tablename} -T show", $descriptorspec, $pipes); + $process = proc_open("/sbin/pfctl -t {$tablename} -T show", $descriptorspec, $pipes); if (is_resource($process)) { $ips = []; while ($ip = fgets($pipes[1], 96)) { @@ -216,7 +216,7 @@ $work_files = (function () use ($is_ten_minutes) { // Triggering TLS-handshake processor when corresponding work file exists. if (in_array(TLS_HANDSHAKE_FILE_WORK, $work_files)) { - mwexec(TLS_HANDSHAKE_PROCESSING_TASK); + mwexecf(TLS_HANDSHAKE_PROCESSING_TASK); } // Abort if permanent ban file is missing diff --git a/www/nginx/src/opnsense/scripts/nginx/setup.php b/www/nginx/src/opnsense/scripts/nginx/setup.php index 96a863c74..e8722930d 100755 --- a/www/nginx/src/opnsense/scripts/nginx/setup.php +++ b/www/nginx/src/opnsense/scripts/nginx/setup.php @@ -120,15 +120,25 @@ if (isset($nginx['http_server'])) { $cert['prv'] ); if (!empty($http_server['ca'])) { - foreach ($http_server['ca'] as $caref) { - $ca = find_ca($caref); - if (isset($ca)) { - export_pem_file( - KEY_DIRECTORY . $hostname . '_ca.pem', - $ca['crt'] - ); + syslog(LOG_DEBUG, "NGINX setup: Setting up the CA certs for {$hostname}."); + $ca_certs = []; + foreach ($http_server['ca'] as $carefs) { + foreach (explode(',', $carefs) as $caref) { + syslog(LOG_DEBUG, "NGINX setup: Searching for {$caref} CA data"); + $ca = find_ca($caref); + if (isset($ca)) { + syslog(LOG_DEBUG, "NGINX setup: client auth CA found. Adding to the list"); + $ca_certs[] = base64_decode($ca['crt']); + } } } + if (count($ca_certs) > 0) { + export_pem_file( + KEY_DIRECTORY . $hostname . '_ca.pem', + '', + implode("\n", $ca_certs) + ); + } } } } diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf index 87592f4b4..f787eff2c 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf @@ -48,6 +48,12 @@ server_names_hash_max_size {{ OPNsense.Nginx.http.server_names_hash_max_size }}; {% if OPNsense.Nginx.http.server_names_hash_bucket_size is defined and OPNsense.Nginx.http.server_names_hash_bucket_size != '' %} server_names_hash_bucket_size {{ OPNsense.Nginx.http.server_names_hash_bucket_size }}; {% endif %} +{% if OPNsense.Nginx.http.variables_hash_max_size is defined and OPNsense.Nginx.http.variables_hash_max_size != '' %} +variables_hash_max_size {{ OPNsense.Nginx.http.variables_hash_max_size }}; +{% endif %} +{% if OPNsense.Nginx.http.variables_hash_bucket_size is defined and OPNsense.Nginx.http.variables_hash_bucket_size != '' %} +variables_hash_bucket_size {{ OPNsense.Nginx.http.variables_hash_bucket_size }}; +{% endif %} {% if OPNsense.Nginx.http.keepalive_timeout is defined and OPNsense.Nginx.http.keepalive_timeout != '' %} keepalive_timeout {{ OPNsense.Nginx.http.keepalive_timeout }}; {% endif %} @@ -117,7 +123,7 @@ server { {% for listen_address in server.listen_https_address.split(',') %} listen {{ listen_address }} ssl{% if server.proxy_protocol is defined and server.proxy_protocol == '1' %} proxy_protocol{% endif %}{% if server.default_server is defined and server.default_server == '1' %} default_server{% endif %}; {% endfor %} - http2 on; + http2 {% if server.http2|default("1") == "1" %}on{% else %}off{% endif %}; {% if server.tls_reject_handshake is defined and server.tls_reject_handshake == '1'%} ssl_reject_handshake on; {% endif %} @@ -246,6 +252,7 @@ server { root /usr/local/etc/nginx/views; } {% endif %} + proxy_intercept_errors {% if server.proxy_intercept_errors|default("0") == "1" %}on{% else %}off{% endif %}; {% if server.security_header is defined and server.security_header != '' %} {% set security_rule = helpers.getUUID(server.security_header) %} {% if security_rule is defined %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf index 9f9b57603..71529378b 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf @@ -47,6 +47,9 @@ location {{ location.matchtype }} {{ location.urlpattern }} { error_page {{ errorpage.statuscodes.replace(',', ' ') }} {% if errorpage.response is defined and errorpage.response != '' %}={{ errorpage.response }} {% endif %}{% if errorpage.redirect is defined and errorpage.redirect != '' %}{{ errorpage.redirect }}{% else %}/error_{{ errorpage_uuid.replace('-', '') }}.html{% endif %}; {% endfor %} {% endif %} +{% if location.proxy_intercept_errors is defined and location.proxy_intercept_errors != 'Inherit' %} + proxy_intercept_errors {{ location.proxy_intercept_errors }}; +{% endif %} {% if location.force_https is defined and location.force_https == '1' %} if ($scheme != "https") { return 302 https://$host$request_uri; @@ -144,7 +147,13 @@ location {{ location.matchtype }} {{ location.urlpattern }} { proxy_cache_use_stale {{ location.cache_use_stale.replace(',', ' ') }}; {% endif %} {% if location.cache_valid is defined and location.cache_valid != '' %} - proxy_cache_valid {{ location.cache_valid }}m; + proxy_cache_valid {{ location.cache_valid }}m; +{% endif %} +{% if location.proxy_cache_valid is defined and location.proxy_cache_valid != '' %} +{% for pcache_valid_uuid in location.proxy_cache_valid.split(',') %} +{% set pcache_valid = helpers.getUUID(pcache_valid_uuid) %} + proxy_cache_valid {{ pcache_valid.code.replace(',', ' ') }} {{ pcache_valid.valid }}m; +{% endfor %} {% endif %} proxy_cache_min_uses {{ location.cache_min_uses|default('1') }}; proxy_cache_background_update {% if location.cache_background_update is defined and location.cache_background_update == '1' %}on{% else %}off{% endif %}; diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf index 9b696220f..9904736ce 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf @@ -93,6 +93,10 @@ proxy_ssl_certificate_key /usr/local/etc/nginx/key/{{ upstream.tls_client_certificate }}.key; proxy_ssl_certificate /usr/local/etc/nginx/key/{{ upstream.tls_client_certificate }}.pem; {% endif %} +{% if upstream.tls_name_override is defined and upstream.tls_name_override != '' %} + proxy_ssl_server_name on; + proxy_ssl_name {{ upstream.tls_name_override }}; +{% endif %} {% endif %} proxy_ssl {% if upstream.tls_enable == '1' %}on{% else %}off{% endif %}; proxy_pass upstream{{ server.upstream.replace('-','') }}; diff --git a/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js b/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js index 3ef498d00..d25e07387 100644 --- a/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js +++ b/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js @@ -1 +1 @@ -!function(t){var e={};function n(i){if(e[i])return e[i].exports;var s=e[i]={i:i,l:!1,exports:{}};return t[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var s in t)n.d(i,s,function(e){return t[e]}.bind(null,s));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=25)}({25:function(t,e,n){"use strict";n.r(e);var i=Backbone.View.extend({tagName:"div",attributes:{class:"container-fluid"},child_views:[],createModel:null,upstreamCollection:null,initialize:function(t){this.dataField=$(t.dataField),this.entryclass=t.entryclass,this.createModel=t.createModel,this.upstreamCollection=t.upstreamCollection,this.listenTo(this.collection,"add remove reset",this.render),this.listenTo(this.collection,"change",this.update),this.dataField.after(this.$el)},events:{"click .add":"addEntry"},render:function(){this.child_views.forEach(t=>t.remove()),this.$el.html(""),this.child_views=[],this.update(),this.collection.each(t=>{const e=new this.entryclass({model:t,collection:this.collection,upstreamCollection:this.upstreamCollection});this.child_views.push(e),this.$el.append(e.$el),e.render()}),this.$el.append($('\n
        \n \n
        '))},update:function(){this.dataField.data("data",this.collection.toJSON())},addEntry:function(t){t.preventDefault(),this.collection.add(this.createModel())}});var s=Backbone.Collection.extend({url:"/api/nginx/settings/searchupstream",parse:function(t){return t.rows}});const a=Backbone.View.extend({tagName:"div",attributes:{class:"row"},events:{"keyup .key":function(){this.model.set("hostname",this.key.value)},"change .value":function(){this.model.set("upstream",this.value.value)},"click .delete":"deleteEntry"},key:null,value:null,delBtn:null,first:null,second:null,third:null,upstreamCollection:null,initialize:function(t){this.upstreamCollection=t.upstreamCollection,this.listenTo(this.upstreamCollection,"update reset add remove",this.regenerate_list),this.first=document.createElement("div"),this.first.classList.add("col-sm-5"),this.key=document.createElement("input"),this.first.append(this.key),this.key.type="text",this.key.classList.add("key"),this.key.value=this.model.get("hostname"),this.second=document.createElement("div"),this.second.classList.add("col-sm-5"),this.value=document.createElement("select"),this.second.append(this.value),this.value.classList.add("value"),this.value.classList.add("form-control"),this.value.value=this.model.get("upstream"),this.third=document.createElement("div"),this.third.classList.add("col-sm-2"),this.third.style.textAlign="right",this.delBtn=document.createElement("button"),this.delBtn.classList.add("delete"),this.delBtn.classList.add("btn"),this.delBtn.innerHTML='',this.third.append(this.delBtn),this.model.has("upstream")&&0!==this.upstreamCollection.where({uuid:this.model.get("upstream")}).length||this.upstreamCollection.length>0&&this.model.set("upstream",this.upstreamCollection.at(0).get("uuid")),this.$el.append(this.first).append(this.second).append(this.third)},render:function(){$(this.key).val(this.model.get("hostname")),this.regenerate_list(),$(this.value).val(this.model.get("upstream"))},deleteEntry:function(t){t.preventDefault(),this.collection.remove(this.model)},regenerate_list:function(){const t=$(this.value);t.html(""),this.upstreamCollection.each(e=>t.append(``)),t.val(this.model.get("upstream")),t.selectpicker("refresh")}}),l=Backbone.View.extend({tagName:"div",attributes:{class:"row"},events:{"keyup .key":function(){this.model.set("network",this.key.value)},"change .value":function(){this.model.set("action",this.value.value)},"click .delete":"deleteEntry"},key:null,value:null,delBtn:null,first:null,second:null,third:null,upstreamCollection:null,initialize:function(t){this.upstreamCollection=t.upstreamCollection,this.listenTo(this.upstreamCollection,"update reset add remove",this.regenerate_list),this.first=document.createElement("div"),this.first.classList.add("col-sm-5"),this.key=document.createElement("input"),this.first.append(this.key),this.key.type="text",this.key.classList.add("key"),this.key.value=this.model.get("network"),this.second=document.createElement("div"),this.second.classList.add("col-sm-5"),this.value=document.createElement("select"),this.second.append(this.value),this.value.classList.add("value"),this.value.classList.add("form-control"),this.value.value=this.model.get("action"),this.third=document.createElement("div"),this.third.classList.add("col-sm-2"),this.third.style.textAlign="right",this.delBtn=document.createElement("button"),this.delBtn.classList.add("delete"),this.delBtn.classList.add("btn"),this.delBtn.innerHTML='',this.third.append(this.delBtn),this.$el.append(this.first).append(this.second).append(this.third)},render:function(){$(this.key).val(this.model.get("network")),this.regenerate_list(),$(this.value).val(this.model.get("action"))},deleteEntry:function(t){t.preventDefault(),this.collection.remove(this.model)},regenerate_list:function(){const t=$(this.value);t.html(""),this.upstreamCollection.each(e=>t.append(``)),t.val(this.model.get("action")),t.selectpicker("refresh")}});var o=Backbone.Collection.extend({initialize:function(){let t=this;$("#snihostname\\.data").change(function(){t.regenerateFromView()})},regenerateFromView:function(){let t=$("#snihostname\\.data").data("data");_.isArray(t)||(t=[]),this.reset(t)}}),r=Backbone.Model.extend({}),d=Backbone.Model.extend({}),c=Backbone.Collection.extend({initialize:function(){let t=this;$("#ipacl\\.data").change(function(){t.regenerateFromView()})},regenerateFromView:function(){let t=$("#ipacl\\.data").data("data");_.isArray(t)||(t=[]),this.reset(t)}});const u=new s,h=new Backbone.Collection([{name:"Deny",value:"deny"},{name:"Allow",value:"allow"}]);$(document).ready(function(){mapDataToFormUI({frm_nginx:"/api/nginx/settings/get"}).done(function(){formatTokenizersUI(),$('select[data-allownew="false"]').selectpicker("refresh"),updateServiceControlUI("nginx")}),""!==window.location.hash&&$('a[href="'+window.location.hash+'"]').click(),$(".nav-tabs a").on("shown.bs.tab",function(t){history.pushState(null,null,t.target.hash)}),$(".reload_btn").click(function(){$(".reloadAct_progress").addClass("fa-spin"),ajaxCall("/api/nginx/service/reconfigure",{},function(){$(".reloadAct_progress").removeClass("fa-spin")})}),$('[id*="save_"]').each(function(){$(this).click(function(){let t=$(this).closest("form").attr("id"),e=$(this).closest("form").attr("data-title");saveFormToEndpoint("/api/nginx/settings/set",t,function(){$("#"+t+"_progress").addClass("fa fa-spinner fa-pulse"),ajaxCall("/api/nginx/service/reconfigure",{},function(n,i){$("#"+t+"_progress").removeClass("fa fa-spinner fa-pulse"),void 0===n||"success"===i&&"ok"===n.status?updateServiceControlUI("nginx"):BootstrapDialog.show({type:BootstrapDialog.TYPE_WARNING,title:e,message:JSON.stringify(n),draggable:!0})})})})}),["upstream","upstreamserver","location","credential","userlist","httpserver","streamserver","httprewrite","custompolicy","security_header","ipacl","limit_zone","cache_path","limit_request_connection","snifwd","errorpage","tls_fingerprint","resolver","syslog_target","naxsirule"].forEach(function(t){$("#grid-"+t).UIBootgrid({search:"/api/nginx/settings/search"+t,get:"/api/nginx/settings/get"+t+"/",set:"/api/nginx/settings/set"+t+"/",add:"/api/nginx/settings/add"+t+"/",del:"/api/nginx/settings/del"+t+"/",commands:{copy_uuid:{method:function(t){navigator.clipboard.writeText($(this).data("row-id"))}}},options:{selection:!1,multiSelect:!1,formatters:{commands:function(t,e){return''},response:function(t,e){return"none"==e.response?"unchanged":e.response},statuscodes:function(t,e){const n=[],i=e.statuscodes.split(",");for(let t of i)n.push(t.substr(0,3));return n.join(", ")}}}})}),bind_naxsi_rule_dl_button(),function(){let t=new i({dataField:document.getElementById("snihostname.data"),upstreamCollection:u,entryclass:a,collection:new o,createModel:function(){return new r({hostname:"localhost"})}});window.snifield=t,t.render(),$("#grid-upstream").on("loaded.rs.jquery.bootgrid",function(){u.fetch()}),u.fetch()}();let t=new i({dataField:document.getElementById("ipacl.data"),upstreamCollection:h,entryclass:l,collection:new c,createModel:function(){return new d({network:"::",action:"deny"})}});window.ipaclfield=t,t.render()})}}); \ No newline at end of file +!function(t){var e={};function n(i){if(e[i])return e[i].exports;var s=e[i]={i:i,l:!1,exports:{}};return t[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var s in t)n.d(i,s,function(e){return t[e]}.bind(null,s));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=25)}({25:function(t,e,n){"use strict";n.r(e);var i=Backbone.View.extend({tagName:"div",attributes:{class:"container-fluid"},child_views:[],createModel:null,upstreamCollection:null,initialize:function(t){this.dataField=$(t.dataField),this.entryclass=t.entryclass,this.createModel=t.createModel,this.upstreamCollection=t.upstreamCollection,this.listenTo(this.collection,"add remove reset",this.render),this.listenTo(this.collection,"change",this.update),this.dataField.after(this.$el)},events:{"click .add":"addEntry"},render:function(){this.child_views.forEach(t=>t.remove()),this.$el.html(""),this.child_views=[],this.update(),this.collection.each(t=>{const e=new this.entryclass({model:t,collection:this.collection,upstreamCollection:this.upstreamCollection});this.child_views.push(e),this.$el.append(e.$el),e.render()}),this.$el.append($('\n
        \n \n
        '))},update:function(){this.dataField.data("data",this.collection.toJSON())},addEntry:function(t){t.preventDefault(),this.collection.add(this.createModel())}});var s=Backbone.Collection.extend({url:"/api/nginx/settings/searchupstream",parse:function(t){return t.rows}});const a=Backbone.View.extend({tagName:"div",attributes:{class:"row"},events:{"keyup .key":function(){this.model.set("hostname",this.key.value)},"change .value":function(){this.model.set("upstream",this.value.value)},"click .delete":"deleteEntry"},key:null,value:null,delBtn:null,first:null,second:null,third:null,upstreamCollection:null,initialize:function(t){this.upstreamCollection=t.upstreamCollection,this.listenTo(this.upstreamCollection,"update reset add remove",this.regenerate_list),this.first=document.createElement("div"),this.first.classList.add("col-sm-5"),this.key=document.createElement("input"),this.first.append(this.key),this.key.type="text",this.key.classList.add("key"),this.key.value=this.model.get("hostname"),this.second=document.createElement("div"),this.second.classList.add("col-sm-5"),this.value=document.createElement("select"),this.second.append(this.value),this.value.classList.add("value"),this.value.classList.add("form-control"),this.value.value=this.model.get("upstream"),this.third=document.createElement("div"),this.third.classList.add("col-sm-2"),this.third.style.textAlign="right",this.delBtn=document.createElement("button"),this.delBtn.classList.add("delete"),this.delBtn.classList.add("btn"),this.delBtn.innerHTML='',this.third.append(this.delBtn),this.model.has("upstream")&&0!==this.upstreamCollection.where({uuid:this.model.get("upstream")}).length||this.upstreamCollection.length>0&&this.model.set("upstream",this.upstreamCollection.at(0).get("uuid")),this.$el.append(this.first).append(this.second).append(this.third)},render:function(){$(this.key).val(this.model.get("hostname")),this.regenerate_list(),$(this.value).val(this.model.get("upstream"))},deleteEntry:function(t){t.preventDefault(),this.collection.remove(this.model)},regenerate_list:function(){const t=$(this.value);t.html(""),this.upstreamCollection.each(e=>t.append(``)),t.val(this.model.get("upstream")),t.selectpicker("refresh")}}),l=Backbone.View.extend({tagName:"div",attributes:{class:"row"},events:{"keyup .key":function(){this.model.set("network",this.key.value)},"change .value":function(){this.model.set("action",this.value.value)},"click .delete":"deleteEntry"},key:null,value:null,delBtn:null,first:null,second:null,third:null,upstreamCollection:null,initialize:function(t){this.upstreamCollection=t.upstreamCollection,this.listenTo(this.upstreamCollection,"update reset add remove",this.regenerate_list),this.first=document.createElement("div"),this.first.classList.add("col-sm-5"),this.key=document.createElement("input"),this.first.append(this.key),this.key.type="text",this.key.classList.add("key"),this.key.value=this.model.get("network"),this.second=document.createElement("div"),this.second.classList.add("col-sm-5"),this.value=document.createElement("select"),this.second.append(this.value),this.value.classList.add("value"),this.value.classList.add("form-control"),this.value.value=this.model.get("action"),this.third=document.createElement("div"),this.third.classList.add("col-sm-2"),this.third.style.textAlign="right",this.delBtn=document.createElement("button"),this.delBtn.classList.add("delete"),this.delBtn.classList.add("btn"),this.delBtn.innerHTML='',this.third.append(this.delBtn),this.$el.append(this.first).append(this.second).append(this.third)},render:function(){$(this.key).val(this.model.get("network")),this.regenerate_list(),$(this.value).val(this.model.get("action"))},deleteEntry:function(t){t.preventDefault(),this.collection.remove(this.model)},regenerate_list:function(){const t=$(this.value);t.html(""),this.upstreamCollection.each(e=>t.append(``)),t.val(this.model.get("action")),t.selectpicker("refresh")}});var o=Backbone.Collection.extend({initialize:function(){let t=this;$("#snihostname\\.data").change(function(){t.regenerateFromView()})},regenerateFromView:function(){let t=$("#snihostname\\.data").data("data");_.isArray(t)||(t=[]),this.reset(t)}}),r=Backbone.Model.extend({}),d=Backbone.Model.extend({}),c=Backbone.Collection.extend({initialize:function(){let t=this;$("#ipacl\\.data").change(function(){t.regenerateFromView()})},regenerateFromView:function(){let t=$("#ipacl\\.data").data("data");_.isArray(t)||(t=[]),this.reset(t)}});const u=new s,h=new Backbone.Collection([{name:"Deny",value:"deny"},{name:"Allow",value:"allow"}]);$(document).ready(function(){mapDataToFormUI({frm_nginx:"/api/nginx/settings/get"}).done(function(){formatTokenizersUI(),$('select[data-allownew="false"]').selectpicker("refresh"),updateServiceControlUI("nginx")}),""!==window.location.hash&&$('a[href="'+window.location.hash+'"]').click(),$(".nav-tabs a").on("shown.bs.tab",function(t){history.pushState(null,null,t.target.hash)}),$(".reload_btn").click(function(){$(".reloadAct_progress").addClass("fa-spin"),ajaxCall("/api/nginx/service/reconfigure",{},function(){$(".reloadAct_progress").removeClass("fa-spin")})}),$('[id*="save_"]').each(function(){$(this).click(function(){let t=$(this).closest("form").attr("id"),e=$(this).closest("form").attr("data-title");saveFormToEndpoint("/api/nginx/settings/set",t,function(){$("#"+t+"_progress").addClass("fa fa-spinner fa-pulse"),ajaxCall("/api/nginx/service/reconfigure",{},function(n,i){$("#"+t+"_progress").removeClass("fa fa-spinner fa-pulse"),void 0===n||"success"===i&&"ok"===n.status?updateServiceControlUI("nginx"):BootstrapDialog.show({type:BootstrapDialog.TYPE_WARNING,title:e,message:JSON.stringify(n),draggable:!0})})})})}),["upstream","upstreamserver","location","credential","userlist","httpserver","streamserver","httprewrite","custompolicy","security_header","ipacl","limit_zone","cache_path","proxy_cache_valid","limit_request_connection","snifwd","errorpage","tls_fingerprint","resolver","syslog_target","naxsirule"].forEach(function(t){$("#grid-"+t).UIBootgrid({search:"/api/nginx/settings/search"+t,get:"/api/nginx/settings/get"+t+"/",set:"/api/nginx/settings/set"+t+"/",add:"/api/nginx/settings/add"+t+"/",del:"/api/nginx/settings/del"+t+"/",commands:{copy_uuid:{method:function(t){navigator.clipboard.writeText($(this).data("row-id"))}}},options:{selection:!1,multiSelect:!1,formatters:{commands:function(t,e){return''},response:function(t,e){return"none"==e.response?"unchanged":e.response},statuscodes:function(t,e){const n=[],i=e.statuscodes.split(",");for(let t of i)n.push(t.substr(0,3));return n.join(", ")}}}})}),bind_naxsi_rule_dl_button(),function(){let t=new i({dataField:document.getElementById("snihostname.data"),upstreamCollection:u,entryclass:a,collection:new o,createModel:function(){return new r({hostname:"localhost"})}});window.snifield=t,t.render(),$("#grid-upstream").on("loaded.rs.jquery.bootgrid",function(){u.fetch()}),u.fetch()}();let t=new i({dataField:document.getElementById("ipacl.data"),upstreamCollection:h,entryclass:l,collection:new c,createModel:function(){return new d({network:"::",action:"deny"})}});window.ipaclfield=t,t.render()})}}); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js b/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js index e61a2469b..4272efdaa 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js @@ -66,6 +66,7 @@ function init_grids() { 'ipacl', 'limit_zone', 'cache_path', + 'proxy_cache_valid', 'limit_request_connection', 'snifwd', 'errorpage', diff --git a/www/squid/Makefile b/www/squid/Makefile index a70692d4f..0678e6fd2 100644 --- a/www/squid/Makefile +++ b/www/squid/Makefile @@ -1,9 +1,7 @@ PLUGIN_NAME= squid -PLUGIN_VERSION= 1.1 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.4 PLUGIN_COMMENT= Squid is a caching proxy for the web PLUGIN_DEPENDS= squid squid-langpack -PLUGIN_TIER= 2 PLUGIN_MAINTAINER= franco@opnsense.org .include "../../Mk/plugins.mk" diff --git a/www/squid/contrib/Makefile b/www/squid/contrib/Makefile new file mode 100644 index 000000000..15529e4b4 --- /dev/null +++ b/www/squid/contrib/Makefile @@ -0,0 +1,5 @@ +PLUGIN_CONTRIB= template_error_pages + +ROOT_template_error_pages= /usr/local/opnsense/data/proxy + +.include "../../../Mk/contrib.mk" diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ACCESS_DENIED.html b/www/squid/contrib/template_error_pages/ERR_ACCESS_DENIED.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ACCESS_DENIED.html rename to www/squid/contrib/template_error_pages/ERR_ACCESS_DENIED.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ACL_TIME_QUOTA_EXCEEDED.html b/www/squid/contrib/template_error_pages/ERR_ACL_TIME_QUOTA_EXCEEDED.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ACL_TIME_QUOTA_EXCEEDED.html rename to www/squid/contrib/template_error_pages/ERR_ACL_TIME_QUOTA_EXCEEDED.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_AGENT_CONFIGURE.html b/www/squid/contrib/template_error_pages/ERR_AGENT_CONFIGURE.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_AGENT_CONFIGURE.html rename to www/squid/contrib/template_error_pages/ERR_AGENT_CONFIGURE.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_AGENT_WPAD.html b/www/squid/contrib/template_error_pages/ERR_AGENT_WPAD.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_AGENT_WPAD.html rename to www/squid/contrib/template_error_pages/ERR_AGENT_WPAD.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CACHE_ACCESS_DENIED.html b/www/squid/contrib/template_error_pages/ERR_CACHE_ACCESS_DENIED.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CACHE_ACCESS_DENIED.html rename to www/squid/contrib/template_error_pages/ERR_CACHE_ACCESS_DENIED.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CACHE_MGR_ACCESS_DENIED.html b/www/squid/contrib/template_error_pages/ERR_CACHE_MGR_ACCESS_DENIED.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CACHE_MGR_ACCESS_DENIED.html rename to www/squid/contrib/template_error_pages/ERR_CACHE_MGR_ACCESS_DENIED.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CANNOT_FORWARD.html b/www/squid/contrib/template_error_pages/ERR_CANNOT_FORWARD.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CANNOT_FORWARD.html rename to www/squid/contrib/template_error_pages/ERR_CANNOT_FORWARD.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CONFLICT_HOST.html b/www/squid/contrib/template_error_pages/ERR_CONFLICT_HOST.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CONFLICT_HOST.html rename to www/squid/contrib/template_error_pages/ERR_CONFLICT_HOST.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CONNECT_FAIL.html b/www/squid/contrib/template_error_pages/ERR_CONNECT_FAIL.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_CONNECT_FAIL.html rename to www/squid/contrib/template_error_pages/ERR_CONNECT_FAIL.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_DIR_LISTING.html b/www/squid/contrib/template_error_pages/ERR_DIR_LISTING.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_DIR_LISTING.html rename to www/squid/contrib/template_error_pages/ERR_DIR_LISTING.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_DNS_FAIL.html b/www/squid/contrib/template_error_pages/ERR_DNS_FAIL.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_DNS_FAIL.html rename to www/squid/contrib/template_error_pages/ERR_DNS_FAIL.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ESI.html b/www/squid/contrib/template_error_pages/ERR_ESI.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ESI.html rename to www/squid/contrib/template_error_pages/ERR_ESI.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FORWARDING_DENIED.html b/www/squid/contrib/template_error_pages/ERR_FORWARDING_DENIED.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FORWARDING_DENIED.html rename to www/squid/contrib/template_error_pages/ERR_FORWARDING_DENIED.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_DISABLED.html b/www/squid/contrib/template_error_pages/ERR_FTP_DISABLED.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_DISABLED.html rename to www/squid/contrib/template_error_pages/ERR_FTP_DISABLED.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_FAILURE.html b/www/squid/contrib/template_error_pages/ERR_FTP_FAILURE.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_FAILURE.html rename to www/squid/contrib/template_error_pages/ERR_FTP_FAILURE.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_FORBIDDEN.html b/www/squid/contrib/template_error_pages/ERR_FTP_FORBIDDEN.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_FORBIDDEN.html rename to www/squid/contrib/template_error_pages/ERR_FTP_FORBIDDEN.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_NOT_FOUND.html b/www/squid/contrib/template_error_pages/ERR_FTP_NOT_FOUND.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_NOT_FOUND.html rename to www/squid/contrib/template_error_pages/ERR_FTP_NOT_FOUND.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_PUT_CREATED.html b/www/squid/contrib/template_error_pages/ERR_FTP_PUT_CREATED.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_PUT_CREATED.html rename to www/squid/contrib/template_error_pages/ERR_FTP_PUT_CREATED.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_PUT_ERROR.html b/www/squid/contrib/template_error_pages/ERR_FTP_PUT_ERROR.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_PUT_ERROR.html rename to www/squid/contrib/template_error_pages/ERR_FTP_PUT_ERROR.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_PUT_MODIFIED.html b/www/squid/contrib/template_error_pages/ERR_FTP_PUT_MODIFIED.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_PUT_MODIFIED.html rename to www/squid/contrib/template_error_pages/ERR_FTP_PUT_MODIFIED.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_UNAVAILABLE.html b/www/squid/contrib/template_error_pages/ERR_FTP_UNAVAILABLE.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_FTP_UNAVAILABLE.html rename to www/squid/contrib/template_error_pages/ERR_FTP_UNAVAILABLE.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_GATEWAY_FAILURE.html b/www/squid/contrib/template_error_pages/ERR_GATEWAY_FAILURE.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_GATEWAY_FAILURE.html rename to www/squid/contrib/template_error_pages/ERR_GATEWAY_FAILURE.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ICAP_FAILURE.html b/www/squid/contrib/template_error_pages/ERR_ICAP_FAILURE.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ICAP_FAILURE.html rename to www/squid/contrib/template_error_pages/ERR_ICAP_FAILURE.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_INVALID_REQ.html b/www/squid/contrib/template_error_pages/ERR_INVALID_REQ.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_INVALID_REQ.html rename to www/squid/contrib/template_error_pages/ERR_INVALID_REQ.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_INVALID_RESP.html b/www/squid/contrib/template_error_pages/ERR_INVALID_RESP.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_INVALID_RESP.html rename to www/squid/contrib/template_error_pages/ERR_INVALID_RESP.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_INVALID_URL.html b/www/squid/contrib/template_error_pages/ERR_INVALID_URL.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_INVALID_URL.html rename to www/squid/contrib/template_error_pages/ERR_INVALID_URL.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_LIFETIME_EXP.html b/www/squid/contrib/template_error_pages/ERR_LIFETIME_EXP.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_LIFETIME_EXP.html rename to www/squid/contrib/template_error_pages/ERR_LIFETIME_EXP.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_NO_RELAY.html b/www/squid/contrib/template_error_pages/ERR_NO_RELAY.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_NO_RELAY.html rename to www/squid/contrib/template_error_pages/ERR_NO_RELAY.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ONLY_IF_CACHED_MISS.html b/www/squid/contrib/template_error_pages/ERR_ONLY_IF_CACHED_MISS.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ONLY_IF_CACHED_MISS.html rename to www/squid/contrib/template_error_pages/ERR_ONLY_IF_CACHED_MISS.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_PRECONDITION_FAILED.html b/www/squid/contrib/template_error_pages/ERR_PRECONDITION_FAILED.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_PRECONDITION_FAILED.html rename to www/squid/contrib/template_error_pages/ERR_PRECONDITION_FAILED.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_PROTOCOL_UNKNOWN.html b/www/squid/contrib/template_error_pages/ERR_PROTOCOL_UNKNOWN.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_PROTOCOL_UNKNOWN.html rename to www/squid/contrib/template_error_pages/ERR_PROTOCOL_UNKNOWN.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_READ_ERROR.html b/www/squid/contrib/template_error_pages/ERR_READ_ERROR.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_READ_ERROR.html rename to www/squid/contrib/template_error_pages/ERR_READ_ERROR.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_READ_TIMEOUT.html b/www/squid/contrib/template_error_pages/ERR_READ_TIMEOUT.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_READ_TIMEOUT.html rename to www/squid/contrib/template_error_pages/ERR_READ_TIMEOUT.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_SECURE_CONNECT_FAIL.html b/www/squid/contrib/template_error_pages/ERR_SECURE_CONNECT_FAIL.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_SECURE_CONNECT_FAIL.html rename to www/squid/contrib/template_error_pages/ERR_SECURE_CONNECT_FAIL.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_SHUTTING_DOWN.html b/www/squid/contrib/template_error_pages/ERR_SHUTTING_DOWN.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_SHUTTING_DOWN.html rename to www/squid/contrib/template_error_pages/ERR_SHUTTING_DOWN.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_SOCKET_FAILURE.html b/www/squid/contrib/template_error_pages/ERR_SOCKET_FAILURE.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_SOCKET_FAILURE.html rename to www/squid/contrib/template_error_pages/ERR_SOCKET_FAILURE.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_TOO_BIG.html b/www/squid/contrib/template_error_pages/ERR_TOO_BIG.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_TOO_BIG.html rename to www/squid/contrib/template_error_pages/ERR_TOO_BIG.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_UNSUP_HTTPVERSION.html b/www/squid/contrib/template_error_pages/ERR_UNSUP_HTTPVERSION.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_UNSUP_HTTPVERSION.html rename to www/squid/contrib/template_error_pages/ERR_UNSUP_HTTPVERSION.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_UNSUP_REQ.html b/www/squid/contrib/template_error_pages/ERR_UNSUP_REQ.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_UNSUP_REQ.html rename to www/squid/contrib/template_error_pages/ERR_UNSUP_REQ.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_URN_RESOLVE.html b/www/squid/contrib/template_error_pages/ERR_URN_RESOLVE.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_URN_RESOLVE.html rename to www/squid/contrib/template_error_pages/ERR_URN_RESOLVE.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_WRITE_ERROR.html b/www/squid/contrib/template_error_pages/ERR_WRITE_ERROR.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_WRITE_ERROR.html rename to www/squid/contrib/template_error_pages/ERR_WRITE_ERROR.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ZERO_SIZE_OBJECT.html b/www/squid/contrib/template_error_pages/ERR_ZERO_SIZE_OBJECT.html similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/ERR_ZERO_SIZE_OBJECT.html rename to www/squid/contrib/template_error_pages/ERR_ZERO_SIZE_OBJECT.html diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/error-details.txt b/www/squid/contrib/template_error_pages/error-details.txt similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/error-details.txt rename to www/squid/contrib/template_error_pages/error-details.txt diff --git a/www/squid/src/opnsense/data/proxy/template_error_pages/errorpage.css b/www/squid/contrib/template_error_pages/errorpage.css similarity index 100% rename from www/squid/src/opnsense/data/proxy/template_error_pages/errorpage.css rename to www/squid/contrib/template_error_pages/errorpage.css diff --git a/www/squid/pkg-descr b/www/squid/pkg-descr index 3ffe8644c..cf30d7de9 100644 --- a/www/squid/pkg-descr +++ b/www/squid/pkg-descr @@ -5,6 +5,22 @@ content serving applications. Plugin Changelog ================ +1.4 + +* Make email_err_data static due to SQUID-2025:2 CVE 10.0 (contributed by m.a.x. it) + +1.3 + +* Repackage template files using new contrib directory feature +* Clean up model and validation messages + +1.2 + +* Change cache_dir from "ufs" to "rock" (contributed by Andy Binder) +* Authenticate before blocklist (contributed by Andy Binder) +* Select behavior for banned hosts (contributed by Andy Binder) +* Do not reject no-bump sites when certificate validation fails (contributed by Michael Muenz) + 1.1 * Syslog corrections and implementation of "workers" (contributed by Andy Binder) diff --git a/www/squid/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/SettingsController.php b/www/squid/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/SettingsController.php index faa8cfc6c..fcca83510 100644 --- a/www/squid/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/SettingsController.php +++ b/www/squid/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/SettingsController.php @@ -113,7 +113,7 @@ class SettingsController extends ApiMutableModelControllerBase * create new cron item for remote acl or return already available one * @return array status action */ - public function fetchRBCronAction() + public function fetchRbCronAction() { $result = array("result" => "failed"); @@ -150,7 +150,7 @@ class SettingsController extends ApiMutableModelControllerBase * search PAC Rule * @return array */ - public function searchPACRuleAction() + public function searchPacRuleAction() { return $this->searchBase('pac.rule', array("enabled", "description", "proxies", "matches"), "description"); } @@ -160,7 +160,7 @@ class SettingsController extends ApiMutableModelControllerBase * @param $uuid item unique id * @return array */ - public function getPACRuleAction($uuid = null) + public function getPacRuleAction($uuid = null) { return array("pac" => $this->getBase('rule', 'pac.rule', $uuid)); } @@ -181,7 +181,7 @@ class SettingsController extends ApiMutableModelControllerBase * @return array result status * @throws \OPNsense\Base\ValidationException */ - public function setPACRuleAction($uuid) + public function setPacRuleAction($uuid) { $this->pac_set_helper(); return $this->setBase('rule', 'pac.rule', $uuid); @@ -192,7 +192,7 @@ class SettingsController extends ApiMutableModelControllerBase * @param $uuid item unique id * @return array status */ - public function togglePACRuleAction($uuid) + public function togglePacRuleAction($uuid) { return $this->toggleBase('pac.rule', $uuid); } @@ -202,7 +202,7 @@ class SettingsController extends ApiMutableModelControllerBase * @param $uuid item unique id * @return array status */ - public function delPACRuleAction($uuid) + public function delPacRuleAction($uuid) { return $this->delBase('pac.rule', $uuid); } @@ -212,7 +212,7 @@ class SettingsController extends ApiMutableModelControllerBase * search PAC Proxy * @return array */ - public function searchPACProxyAction() + public function searchPacProxyAction() { return $this->searchBase('pac.proxy', array("enabled","proxy_type", "name", "url", "description"), "description"); } @@ -222,7 +222,7 @@ class SettingsController extends ApiMutableModelControllerBase * @param $uuid item unique id * @return array */ - public function getPACProxyAction($uuid = null) + public function getPacProxyAction($uuid = null) { return array("pac" => $this->getBase('proxy', 'pac.proxy', $uuid)); } @@ -231,7 +231,7 @@ class SettingsController extends ApiMutableModelControllerBase * add new PAC Proxy and set with attributes from post * @return array */ - public function addPACProxyAction() + public function addPacProxyAction() { $this->pac_set_helper(); return $this->addBase('proxy', 'pac.proxy'); @@ -243,7 +243,7 @@ class SettingsController extends ApiMutableModelControllerBase * @return array result status * @throws \OPNsense\Base\ValidationException */ - public function setPACProxyAction($uuid) + public function setPacProxyAction($uuid) { $this->pac_set_helper(); return $this->setBase('proxy', 'pac.proxy', $uuid); @@ -254,7 +254,7 @@ class SettingsController extends ApiMutableModelControllerBase * @param $uuid item unique id * @return array status */ - public function delPACProxyAction($uuid) + public function delPacProxyAction($uuid) { return $this->delBase('pac.proxy', $uuid); } @@ -263,7 +263,7 @@ class SettingsController extends ApiMutableModelControllerBase * search PAC Match * @return array */ - public function searchPACMatchAction() + public function searchPacMatchAction() { return $this->searchBase('pac.match', array("enabled", "name", "description", "negate", "match_type"), "name"); } @@ -273,7 +273,7 @@ class SettingsController extends ApiMutableModelControllerBase * @param $uuid item unique id * @return array */ - public function getPACMatchAction($uuid = null) + public function getPacMatchAction($uuid = null) { return array("pac" => $this->getBase('match', 'pac.match', $uuid)); } @@ -282,7 +282,7 @@ class SettingsController extends ApiMutableModelControllerBase * add new PAC Proxy and set with attributes from post * @return array */ - public function addPACMatchAction() + public function addPacMatchAction() { $this->pac_set_helper(); return $this->addBase('match', 'pac.match'); @@ -294,7 +294,7 @@ class SettingsController extends ApiMutableModelControllerBase * @return array result status * @throws \OPNsense\Base\ValidationException */ - public function setPACMatchAction($uuid) + public function setPacMatchAction($uuid) { $this->pac_set_helper(); return $this->setBase('match', 'pac.match', $uuid); @@ -305,7 +305,7 @@ class SettingsController extends ApiMutableModelControllerBase * @param $uuid item unique id * @return array status */ - public function delPACMatchAction($uuid) + public function delPacMatchAction($uuid) { return $this->delBase('pac.match', $uuid); } diff --git a/www/squid/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/main.xml b/www/squid/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/main.xml index 998bd76b4..8c01e6af7 100644 --- a/www/squid/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/main.xml +++ b/www/squid/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/main.xml @@ -148,17 +148,24 @@ true - proxy.general.cache.local.l1 - + proxy.general.cache.local.swap_timeout + text - Enter the number of first-level subdirectories for the local cache (default is 16). + Prevents Squid from reading/writing to disk if the operation exceeds the specified timelimit in milliseconds (default 0 = disable when left empty). true - proxy.general.cache.local.l2 - + proxy.general.cache.local.max_swap_rate + text - Enter the number of second-level subdirectories for the local cache (default is 256). + Limits disk access by setting a maximum I/O rate in swaps per second (default 0 = disable when left empty). + true + + + proxy.general.cache.local.slot_size + + text + Defines the size of a database record used to store cached responses. Value should be a multiple of the OS I/O page size (default 16384 when left empty). true @@ -343,7 +350,7 @@ proxy.forward.workers text - Start N main Squid process daemons (i.e., SMP mode). Requires Restart. Do not enable when using local cache. + Start N main Squid process daemons (i.e., SMP mode). Requires Restart. 1 true @@ -414,6 +421,12 @@ Type IP addresses you want to deny access to the proxy server.true + + proxy.forward.acl.allowWhitelistBannedHosts + + checkbox + Allows banned hosts to access domains listed in whitelist. + proxy.forward.acl.whiteList diff --git a/www/squid/src/opnsense/mvc/app/models/OPNsense/Proxy/Proxy.xml b/www/squid/src/opnsense/mvc/app/models/OPNsense/Proxy/Proxy.xml index 3d77eabfe..d87028240 100644 --- a/www/squid/src/opnsense/mvc/app/models/OPNsense/Proxy/Proxy.xml +++ b/www/squid/src/opnsense/mvc/app/models/OPNsense/Proxy/Proxy.xml @@ -1,6 +1,6 @@ //OPNsense/proxy - 1.0.7 + 1.0.9 Squid web proxy settings @@ -18,7 +18,7 @@ 1 65535 - ICP port needs to be an integer value between 1 and 65535 + ICP port needs to be an integer value between 1 and 65535. @@ -97,18 +97,18 @@ 256 0 - Specify a positive memory cache size. (number of MB's) + Specify a positive memory cache size (number of MBs). Y 1 99999 - Specify a maximum object size. (number of MB's) + Specify a maximum object size (number of MBs). 1 99999 - Specify a maximum object size in memory. (number of KB's) + Specify a maximum object size in memory (number of KBs). Default @@ -121,21 +121,21 @@ 100 1 - Specify a positive cache size. (number of MB's) + Specify a positive cache size (number of MBs). Y - - 16 - 1 - Specify a positive number of first-level subdirectories. - Y - - - 256 - 1 - Specify a positive number of second-level subdirectories. - Y - + + 0 + Specify a valid swap-timeout. + + + 0 + Specify a valid swap-rate. + + + 4096 + Specify a multiple of operating system I/O page size. + 0 Y @@ -164,7 +164,7 @@ Specify the overall bandwidth for downloads in kilobits per second. - Both throttling parameters should either be filled or empty + Both throttling parameters should either be filled or empty. AllOrNoneConstraint perHostTrotteling @@ -242,14 +242,14 @@ 3128 1 65535 - Proxy port needs to be an integer value between 1 and 65535 + Proxy port needs to be an integer value between 1 and 65535. Y 3129 1 65535 - SSL Proxy port needs to be an integer value between 1 and 65535 + SSL Proxy port needs to be an integer value between 1 and 65535. Y @@ -257,7 +257,7 @@ Y - When enabling "Log SNI information only", SSL inspection must also be enabled + When enabling "Log SNI information only", SSL inspection must also be enabled. DependConstraint sslurlonly @@ -276,30 +276,30 @@ ca - Please select a valid certificate from the list + Please select a valid certificate from the list. /^([a-zA-Z0-9\.:\[\]\s\-]*?,)*([a-zA-Z0-9\.:\[\]\s\-]*)$/ - Please enter ip addresses or domain names here + Please enter ip addresses or domain names here. 1 100 - worker number needs to be an integer value between 1 and 100 + Worker number needs to be an integer value between 1 and 100. Y 4 1 65535 - max size needs to be an integer value between 1 and 65535 + Maximum size needs to be an integer value between 1 and 65535. Y 5 1 32 - the number of sslrtd children needs to be an integer value between 1 and 32 + The number of sslrtd children needs to be an integer value between 1 and 32. 0 @@ -308,7 +308,7 @@ 1 65535 - SNMP port needs to be an integer value between 1 and 65535 + SNMP port needs to be an integer value between 1 and 65535. Y 3401 @@ -327,7 +327,7 @@ 2121 1 65535 - FTP Proxy port needs to be an integer value between 1 and 65535 + FTP Proxy port needs to be an integer value between 1 and 65535. Y @@ -352,13 +352,17 @@ /^([\/0-9a-fA-F.:,])*/u + + 1 + Y + /^([a-zA-Z0-9]){0,}\.([a-zA-Z0-9].){0,}/ - Please enter a valid domain name here + Please enter a valid domain name here. @@ -389,7 +393,7 @@ The filename may only contain letters, digits and one dot (not required). - Filename should be unique + Filename should be unique. UniqueConstraint @@ -430,7 +434,7 @@ - Related cron not found + Related cron not found. @@ -484,11 +488,11 @@ 1 - Credentials TTL needs to be an integer value above 0 + Credentials TTL needs to be an integer value above 0. 1 - Number of children needs to be an integer value above 0 + Number of children needs to be an integer value above 0. @@ -499,7 +503,7 @@ The proxy name must be set. - Proxy name should be unique + Proxy name should be unique. UniqueConstraint @@ -529,7 +533,7 @@ The match name must be set. - Match name should be unique + Match name should be unique. UniqueConstraint @@ -577,7 +581,7 @@ 0 23 - The last hour of the day is 23! + The last hour of the day is 23. Y @@ -688,7 +692,7 @@ diff --git a/www/squid/src/opnsense/mvc/app/views/OPNsense/Proxy/index.volt b/www/squid/src/opnsense/mvc/app/views/OPNsense/Proxy/index.volt index bda232e0d..d8c8fe40b 100644 --- a/www/squid/src/opnsense/mvc/app/views/OPNsense/Proxy/index.volt +++ b/www/squid/src/opnsense/mvc/app/views/OPNsense/Proxy/index.volt @@ -52,20 +52,20 @@ *************************************************************************************************************/ $("#grid-remote-blacklists").UIBootgrid( - { 'search':'/api/proxy/settings/searchRemoteBlacklists', - 'get':'/api/proxy/settings/getRemoteBlacklist/', - 'set':'/api/proxy/settings/setRemoteBlacklist/', - 'add':'/api/proxy/settings/addRemoteBlacklist/', - 'del':'/api/proxy/settings/delRemoteBlacklist/', - 'toggle':'/api/proxy/settings/toggleRemoteBlacklist/' + { 'search':'/api/proxy/settings/search_remote_blacklists', + 'get':'/api/proxy/settings/get_remote_blacklist/', + 'set':'/api/proxy/settings/set_remote_blacklist/', + 'add':'/api/proxy/settings/add_remote_blacklist/', + 'del':'/api/proxy/settings/del_remote_blacklist/', + 'toggle':'/api/proxy/settings/toggle_remote_blacklist/' } ); $("#grid-pac-match").UIBootgrid( - { 'search':'/api/proxy/settings/searchPACMatch', - 'get':'/api/proxy/settings/getPACMatch/', - 'set':'/api/proxy/settings/setPACMatch/', - 'add':'/api/proxy/settings/addPACMatch/', - 'del':'/api/proxy/settings/delPACMatch/', + { 'search':'/api/proxy/settings/search_pac_match', + 'get':'/api/proxy/settings/get_pac_match/', + 'set':'/api/proxy/settings/set_pac_match/', + 'add':'/api/proxy/settings/add_pac_match/', + 'del':'/api/proxy/settings/del_pac_match/', 'options': { responseHandler: function (response) { // concatenate fields for not. @@ -81,20 +81,20 @@ } ); $("#grid-pac-rule").UIBootgrid( - { 'search':'/api/proxy/settings/searchPACRule', - 'get':'/api/proxy/settings/getPACRule/', - 'set':'/api/proxy/settings/setPACRule/', - 'add':'/api/proxy/settings/addPACRule/', - 'del':'/api/proxy/settings/delPACRule/', - 'toggle':'/api/proxy/settings/togglePACRule/' + { 'search':'/api/proxy/settings/search_pac_rule', + 'get':'/api/proxy/settings/get_pac_rule/', + 'set':'/api/proxy/settings/set_pac_rule/', + 'add':'/api/proxy/settings/add_pac_rule/', + 'del':'/api/proxy/settings/del_pac_rule/', + 'toggle':'/api/proxy/settings/toggle_pac_rule/' } ); $("#grid-pac-proxy").UIBootgrid( - { 'search':'/api/proxy/settings/searchPACProxy', - 'get':'/api/proxy/settings/getPACProxy/', - 'set':'/api/proxy/settings/setPACProxy/', - 'add':'/api/proxy/settings/addPACProxy/', - 'del':'/api/proxy/settings/delPACProxy/' + { 'search':'/api/proxy/settings/search_pac_proxy', + 'get':'/api/proxy/settings/get_pac_proxy/', + 'set':'/api/proxy/settings/set_pac_proxy/', + 'add':'/api/proxy/settings/add_pac_proxy/', + 'del':'/api/proxy/settings/del_pac_proxy/' } ); @@ -166,7 +166,7 @@ $('.reload-pac-btn').click(function () { $('.reload-pac-btn .fa-refresh').addClass('fa-spin'); - ajaxCall("/api/proxy/service/refreshTemplate", {}, function(data,status) { + ajaxCall("/api/proxy/service/refresh_template", {}, function(data,status) { $('.reload-pac-btn .fa-refresh').removeClass('fa-spin'); }); }); @@ -192,7 +192,7 @@ */ $("#ScheduleAct").click(function() { $("#scheduleAct_progress").addClass("fa fa-spinner fa-pulse"); - ajaxCall("/api/proxy/settings/fetchRBCron", {}, function(data,status) { + ajaxCall("/api/proxy/settings/fetch_rb_cron", {}, function(data,status) { $("#scheduleAct_progress").removeClass("fa fa-spinner fa-pulse"); if (data.uuid !=undefined) { // redirect to cron page diff --git a/www/squid/src/opnsense/service/templates/OPNsense/Proxy/squid.acl.conf b/www/squid/src/opnsense/service/templates/OPNsense/Proxy/squid.acl.conf index b9e1f8787..b30258cb3 100644 --- a/www/squid/src/opnsense/service/templates/OPNsense/Proxy/squid.acl.conf +++ b/www/squid/src/opnsense/service/templates/OPNsense/Proxy/squid.acl.conf @@ -14,6 +14,20 @@ adaptation_access request_mod allow unrestricted http_access allow unrestricted {% endif %} +{% if helpers.exists('OPNsense.proxy.forward.acl.bannedHosts') and OPNsense.proxy.forward.acl.allowWhitelistBannedHosts|default('1') == '0' %} + +# ACL list (Deny) banned hosts +{% if helpers.exists('OPNsense.proxy.forward.icap.enable') and OPNsense.proxy.forward.icap.enable == '1' %} +{% if helpers.exists('OPNsense.proxy.forward.icap.ResponseURL') %} +adaptation_access response_mod deny bannedHosts +{% endif %} +{% if helpers.exists('OPNsense.proxy.forward.icap.RequestURL') %} +adaptation_access request_mod deny bannedHosts +{% endif %} +{% endif %} +http_access deny bannedHosts +{% endif %} + {% if helpers.exists('OPNsense.proxy.forward.acl.whiteList') %} # ACL list (Allow) whitelist @@ -28,6 +42,10 @@ adaptation_access request_mod allow whiteList http_access allow whiteList {% endif %} +{% if not helpers.empty('OPNsense.proxy.forward.authentication.method') %} +http_access deny !local_auth +{% endif %} + {% if helpers.exists('OPNsense.proxy.forward.acl.blackList') %} # @@ -135,7 +153,9 @@ adaptation_access request_mod deny CONNECT !SSL_ports {% if helpers.exists('OPNs http_access deny CONNECT !SSL_ports {% if helpers.exists('OPNsense.proxy.forward.acl.unrestricted') %}!unrestricted{% endif %} -{% if helpers.exists('OPNsense.proxy.forward.acl.bannedHosts') %} +{% if helpers.exists('OPNsense.proxy.forward.acl.bannedHosts') and OPNsense.proxy.forward.acl.allowWhitelistBannedHosts|default('1') == '1' %} + +# ACL list (Deny) banned hosts {% if helpers.exists('OPNsense.proxy.forward.icap.enable') and OPNsense.proxy.forward.icap.enable == '1' %} {% if helpers.exists('OPNsense.proxy.forward.icap.ResponseURL') %} adaptation_access response_mod deny bannedHosts diff --git a/www/squid/src/opnsense/service/templates/OPNsense/Proxy/squid.conf b/www/squid/src/opnsense/service/templates/OPNsense/Proxy/squid.conf index d42e8dfa7..1134b1a9e 100644 --- a/www/squid/src/opnsense/service/templates/OPNsense/Proxy/squid.conf +++ b/www/squid/src/opnsense/service/templates/OPNsense/Proxy/squid.conf @@ -84,6 +84,7 @@ ssl_bump peek bump_step2 bump_nobumpsites ssl_bump splice bump_step3 bump_nobumpsites ssl_bump stare bump_step2 ssl_bump bump bump_step3 +sslproxy_cert_error allow bump_nobumpsites {% endif %} sslproxy_cert_error deny all @@ -320,7 +321,7 @@ maximum_object_size_in_memory {{OPNsense.proxy.general.cache.local.maximum_objec memory_cache_mode {{OPNsense.proxy.general.cache.local.memory_cache_mode}} {% endif %} {% if OPNsense.proxy.general.cache.local.enabled == '1' %} -cache_dir ufs {{OPNsense.proxy.general.cache.local.directory}} {{OPNsense.proxy.general.cache.local.size}} {{OPNsense.proxy.general.cache.local.l1}} {{OPNsense.proxy.general.cache.local.l2}} +cache_dir rock {{OPNsense.proxy.general.cache.local.directory}} {{OPNsense.proxy.general.cache.local.size}}{% if not helpers.empty('OPNsense.proxy.general.cache.local.swap_timeout') %} swap-timeout={{OPNsense.proxy.general.cache.local.swap_timeout}}{% endif %}{% if not helpers.empty('OPNsense.proxy.general.cache.local.max_swap_rate') %} max-swap-rate={{OPNsense.proxy.general.cache.local.max_swap_rate}}{% endif %}{% if not helpers.empty('OPNsense.proxy.general.cache.local.slot_size') %} slot-size={{OPNsense.proxy.general.cache.local.slot_size}}{% endif %} {% endif %} {% endif %} {% endif %} @@ -328,6 +329,9 @@ cache_dir ufs {{OPNsense.proxy.general.cache.local.directory}} {{OPNsense.proxy. # Leave coredumps in the first cache dir coredump_dir /var/squid/cache +# Disable Debug information in Administartor Email / SQUID-2025:2 +email_err_data off + # # Add any of your own refresh_pattern entries above these. # diff --git a/www/web-proxy-sso/src/opnsense/mvc/app/models/OPNsense/ProxySSO/ProxySSO.xml b/www/web-proxy-sso/src/opnsense/mvc/app/models/OPNsense/ProxySSO/ProxySSO.xml index f43f0389e..963d01260 100644 --- a/www/web-proxy-sso/src/opnsense/mvc/app/models/OPNsense/ProxySSO/ProxySSO.xml +++ b/www/web-proxy-sso/src/opnsense/mvc/app/models/OPNsense/ProxySSO/ProxySSO.xml @@ -5,11 +5,11 @@ - 0 + 0 Y - W2008 + W2008 Y Windows 2003