net/haproxy: add support for maps, closes #180

This commit is contained in:
Frank Wall
2018-08-06 23:28:40 +02:00
parent 6401ba930a
commit 80e4b5f596
10 changed files with 307 additions and 2 deletions
@@ -1005,4 +1005,102 @@ class SettingsController extends ApiControllerBase
"name"
);
}
/**
* retrieve mapfile settings or return defaults
* @param $uuid item unique id
* @return array
*/
public function getMapfileAction($uuid = null)
{
$mdlCP = new HAProxy();
if ($uuid != null) {
$node = $mdlCP->getNodeByReference('mapfiles.mapfile.'.$uuid);
if ($node != null) {
// return node
return array("mapfile" => $node->getNodes());
}
} else {
// generate new node, but don't save to disc
$node = $mdlCP->mapfiles->mapfile->add();
return array("mapfile" => $node->getNodes());
}
return array();
}
/**
* update mapfile with given properties
* @param $uuid item unique id
* @return array
*/
public function setMapfileAction($uuid)
{
if ($this->request->isPost() && $this->request->hasPost("mapfile")) {
$mdlCP = new HAProxy();
if ($uuid != null) {
$node = $mdlCP->getNodeByReference('mapfiles.mapfile.'.$uuid);
if ($node != null) {
$node->setNodes($this->request->getPost("mapfile"));
return $this->save($mdlCP, $node, "mapfile");
}
}
}
return array("result"=>"failed");
}
/**
* add new mapfile and set with attributes from post
* @return array
*/
public function addMapfileAction()
{
$result = array("result"=>"failed");
if ($this->request->isPost() && $this->request->hasPost("mapfile")) {
$mdlCP = new HAProxy();
$node = $mdlCP->mapfiles->mapfile->Add();
$node->setNodes($this->request->getPost("mapfile"));
return $this->save($mdlCP, $node, "mapfile");
}
return $result;
}
/**
* delete mapfile by uuid
* @param $uuid item unique id
* @return array status
*/
public function delMapfileAction($uuid)
{
$result = array("result"=>"failed");
if ($this->request->isPost()) {
$mdlCP = new HAProxy();
if ($uuid != null) {
if ($mdlCP->mapfiles->mapfile->del($uuid)) {
// if item is removed, serialize to config and save
$mdlCP->serializeToConfig();
Config::getInstance()->save();
$result['result'] = 'deleted';
} else {
$result['result'] = 'not found';
}
}
}
return $result;
}
/**
* search mapfiles
* @return array
*/
public function searchMapfilesAction()
{
$this->sessionClose();
$mdlCP = new HAProxy();
$grid = new UIModelGrid($mdlCP->mapfiles->mapfile);
return $grid->fetchBindRequest(
$this->request,
array("name", "description"),
"name"
);
}
}
@@ -53,6 +53,7 @@ class IndexController extends \OPNsense\Base\IndexController
$this->view->formDialogAcl = $this->getForm("dialogAcl");
$this->view->formDialogLua = $this->getForm("dialogLua");
$this->view->formDialogErrorfile = $this->getForm("dialogErrorfile");
$this->view->formDialogMapfile = $this->getForm("dialogMapfile");
// set additional view parameters
$mdlHAProxy = new \OPNsense\HAProxy\HAProxy();
$this->view->showIntro = (string)$mdlHAProxy->general->showIntro;
@@ -354,4 +354,21 @@
<type>textbox</type>
<help><![CDATA[Specify a HAProxy rule/ACL that is currently not supported by the GUI.]]></help>
</field>
<field>
<label>Parameters</label>
<type>header</type>
<style>type_table table_map_use_backend</style>
</field>
<field>
<id>action.map_use_backend_file</id>
<label>Map file</label>
<type>dropdown</type>
<help><![CDATA[HAProxy will extract the Host header from the HTTP request and search the map file for a match. If a match is found, the backend pool from the map file will be used.]]></help>
</field>
<field>
<id>action.map_use_backend_default</id>
<label>Default backend pool</label>
<type>dropdown</type>
<help><![CDATA[HAProxy will use this backend pool if no match is found in the map file.]]></help>
</field>
</form>
@@ -0,0 +1,20 @@
<form>
<field>
<id>mapfile.name</id>
<label>Name</label>
<type>text</type>
<help>Name to identify this map file.</help>
</field>
<field>
<id>mapfile.description</id>
<label>Description</label>
<type>text</type>
<help>Description for this map file.</help>
</field>
<field>
<id>mapfile.content</id>
<label>Content</label>
<type>textbox</type>
<help><![CDATA[Paste the content of your map file here. See the <a target="_blank" href="http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#map">HAProxy documentation</a> for a full description.]]></help>
</field>
</form>
@@ -1565,6 +1565,7 @@
<OptionValues>
<use_backend>Use specified Backend Pool</use_backend>
<use_server>Override server in Backend Pool</use_server>
<map_use_backend>Map domains to backend pools using a map file</map_use_backend>
<http-request_allow>http-request allow</http-request_allow>
<http-request_deny>http-request deny</http-request_deny>
<http-request_tarpit>http-request tarpit</http-request_tarpit>
@@ -1781,6 +1782,30 @@
<actionValue type="TextField">
<Required>N</Required>
</actionValue>
<map_use_backend_file type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>mapfiles.mapfile</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related map file item not found</ValidationMessage>
<Multiple>N</Multiple>
<Required>N</Required>
</map_use_backend_file>
<map_use_backend_default type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>backends.backend</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related backend pool item not found</ValidationMessage>
<Multiple>N</Multiple>
<Required>N</Required>
</map_use_backend_default>
</action>
</actions>
<luas>
@@ -1843,5 +1868,25 @@
</content>
</errorfile>
</errorfiles>
<mapfiles>
<mapfile type="ArrayField">
<id type="UniqueIdField">
<Required>Y</Required>
</id>
<name type="TextField">
<mask>/^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
<Required>Y</Required>
</name>
<description type="TextField">
<mask>/^.{1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
<Required>N</Required>
</description>
<content type="TextField">
<Required>Y</Required>
</content>
</mapfile>
</mapfiles>
</items>
</model>
@@ -16,6 +16,7 @@
<Acls VisibleName="ACLs" url="/ui/haproxy#acls"/>
<Luas VisibleName="Lua Scripts" url="/ui/haproxy#luas"/>
<Errorfiles VisibleName="Error Files" url="/ui/haproxy#errorfiles"/>
<Mapfiles VisibleName="Map Files" url="/ui/haproxy#mapfiles"/>
</Settings>
<Statistics order="20" url="/ui/haproxy/statistics">
<Overview VisibleName="Overview" url="/ui/haproxy/statistics#info"/>
@@ -146,6 +146,18 @@ POSSIBILITY OF SUCH DAMAGE.
}
);
$("#grid-mapfiles").UIBootgrid(
{ search:'/api/haproxy/settings/searchMapfiles',
get:'/api/haproxy/settings/getMapfile/',
set:'/api/haproxy/settings/setMapfile/',
add:'/api/haproxy/settings/addMapfile/',
del:'/api/haproxy/settings/delMapfile/',
options: {
rowCount:[10,25,50,100,500,1000]
}
}
);
// hook into on-show event for dialog to extend layout.
$('#DialogAcl').on('shown.bs.modal', function (e) {
$("#acl\\.expression").change(function(){
@@ -463,6 +475,7 @@ POSSIBILITY OF SUCH DAMAGE.
{% endif %}
<li><a data-toggle="tab" id="errorfiles-tab" href="#errorfiles">{{ lang._('Error Messages') }}</a></li>
<li><a data-toggle="tab" href="#luas">{{ lang._('Lua Scripts') }}</a></li>
<li><a data-toggle="tab" href="#mapfiles">{{ lang._('Map Files') }}</a></li>
</ul>
</li>
</ul>
@@ -535,8 +548,9 @@ POSSIBILITY OF SUCH DAMAGE.
<ul>
<li>{{ lang._("%sError Messages:%s Return a custom message instead of errors generated by HAProxy. Useful to overwrite HAProxy's internal error messages. The message must represent the full HTTP response and include required HTTP headers.") | format('<b>', '</b>') }}</li>
<li>{{ lang._("%sLua scripts:%s Include your own Lua code/scripts to extend HAProxy's functionality. The Lua code can be used in certain %sRules%s, for example.") | format('<b>', '</b>', '<b>', '</b>') }}</li>
<li>{{ lang._("%sMap Files:%s A map allows to map a data in input to an other one on output. For example, this makes it possible to map a large number of domains to backend pools without using the GUI. Map files need to be used in %sRules%s, otherwise they are ignored.") | format('<b>', '</b>', '<b>', '</b>') }}</li>
</ul>
<p>{{ lang._("For more details visit HAProxy's official documentation regarding the %sError Messages%s and the %sLua Script%s features.") | format('<a href="http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4-errorfile" target="_blank">', '</a>', '<a href="http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#lua-load" target="_blank">', '</a>') }}</p>
<p>{{ lang._("For more details visit HAProxy's official documentation regarding the %sError Messages%s, %sLua Script%s and the %sMap Files%s features.") | format('<a href="http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4-errorfile" target="_blank">', '</a>', '<a href="http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#lua-load" target="_blank">', '</a>', '<a href="http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#map" target="_blank">', '</a>') }}</p>
<br/>
</div>
</div>
@@ -834,6 +848,40 @@ POSSIBILITY OF SUCH DAMAGE.
<br/>
</div>
</div>
<div id="mapfiles" class="tab-pane fade">
<!-- tab page "mapfiles" -->
<table id="grid-mapfiles" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogMapfile">
<thead>
<tr>
<th data-column-id="mapfileid" data-type="number" data-visible="false">{{ lang._('Map File ID') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Map File Name') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
<!-- apply button -->
<div class="col-md-12">
<hr/>
<button class="btn btn-primary" id="reconfigureAct-mapfiles" type="button"><b>{{ lang._('Apply') }}</b><i id="reconfigureAct_progress" class=""></i></button>
<button class="btn btn-primary" id="configtestAct-mapfiles" type="button"><b>{{ lang._('Test syntax') }}</b><i id="configtestAct_progress" class=""></i></button>
<br/>
<br/>
</div>
</div>
</div>
{# include dialogs #}
@@ -845,3 +893,4 @@ POSSIBILITY OF SUCH DAMAGE.
{{ partial("layout_partials/base_dialog",['fields':formDialogAcl,'id':'DialogAcl','label':lang._('Edit Condition')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogLua,'id':'DialogLua','label':lang._('Edit Lua Script')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogErrorfile,'id':'DialogErrorfile','label':lang._('Edit Error Message')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogMapfile,'id':'DialogMapfile','label':lang._('Edit Map File')])}}
@@ -0,0 +1,55 @@
#!/usr/local/bin/php
<?php
/**
* Copyright (C) 2016-2018 Frank Wall
* Copyright (C) 2015 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.
*
*/
// Use legacy code to export certificates to the filesystem.
require_once("config.inc");
require_once("certs.inc");
require_once("legacy_bindings.inc");
use OPNsense\Core\Config;
global $config;
// traverse HAProxy map files
$configObj = Config::getInstance()->object();
if (isset($configObj->OPNsense->HAProxy->mapfiles)) {
foreach ($configObj->OPNsense->HAProxy->mapfiles->children() as $mapfile) {
$mf_name = (string)$mapfile->name;
$mf_id = (string)$mapfile->id;
if ($mf_id != "") {
$mf_content = htmlspecialchars_decode(str_replace("\r", "", (string)$mapfile->content));
$mf_filename = "/var/etc/haproxy/mapfiles/" . $mf_id . ".txt";
file_put_contents($mf_filename, $mf_content);
chmod($mf_filename, 0600);
echo "map file exported to " . $mf_filename . "\n";
}
}
}
@@ -1,7 +1,7 @@
#!/bin/sh
# NOTE: Keep /var/haproxy on this list, see GH issue opnsense/plugins #39.
HAPROXY_DIRS="/var/haproxy /var/haproxy/var/run /var/etc/haproxy/ssl /var/etc/haproxy/lua /var/etc/haproxy/errorfiles"
HAPROXY_DIRS="/var/haproxy /var/haproxy/var/run /var/etc/haproxy/ssl /var/etc/haproxy/lua /var/etc/haproxy/errorfiles /var/etc/haproxy/mapfiles"
for directory in ${HAPROXY_DIRS}; do
mkdir -p ${directory}
@@ -16,5 +16,6 @@ find /var/haproxy -type d -exec chmod 550 {} \;
/usr/local/opnsense/scripts/OPNsense/HAProxy/exportCerts.php > /dev/null 2>&1
/usr/local/opnsense/scripts/OPNsense/HAProxy/exportLuaScripts.php > /dev/null 2>&1
/usr/local/opnsense/scripts/OPNsense/HAProxy/exportErrorFiles.php > /dev/null 2>&1
/usr/local/opnsense/scripts/OPNsense/HAProxy/exportMapFiles.php > /dev/null 2>&1
exit 0
@@ -306,6 +306,24 @@
{% set action_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif action_data.type == 'map_use_backend' %}
{# # First get the map file path #}
{% if action_data.map_use_backend_file|default("") != "" %}
{% set mapfile_data = helpers.getUUID(action_data.map_use_backend_file) %}
{% set mapfile_path = '/var/etc/haproxy/mapfiles/' ~ mapfile_data.id ~ '.txt' %}
{# # Check if a default backend is specified #}
{% if action_data.map_use_backend_default|default("") != "" %}
{% set defaultbackend_data = helpers.getUUID(action_data.map_use_backend_default) %}
{% set defaultbackend_option = ',' ~ defaultbackend_data.name %}
{% else %}
{% set defaultbackend_option = '' %}
{% endif %}
{# # Finally add map file to config #}
{% do action_options.append('use_backend %[req.hdr(host),lower,map_dom(' ~ mapfile_path ~ defaultbackend_option ~ ')]') %}
{% else %}
{% set action_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif action_data.type == 'http-request_allow' %}
{% do action_options.append('http-request allow') %}
{% elif action_data.type == 'http-request_deny' %}