add: client configuration templates (fix #155)

This commit is contained in:
ziirish 2018-01-29 22:35:45 +01:00
parent fad4cf2898
commit 5279f0b019
No known key found for this signature in database
GPG key ID: 72DB229A64B54E46
16 changed files with 642 additions and 141 deletions

View file

@ -9,6 +9,7 @@ Current
- **BREAKING**: the *running* backups are now displayed in ``green`` instead of ``blue`` - **BREAKING**: the *running* backups are now displayed in ``green`` instead of ``blue``
- Add: new plugins system to allow users to write their own modules - Add: new plugins system to allow users to write their own modules
- Add: `Italian translation <https://git.ziirish.me/ziirish/burp-ui/merge_requests/74>`_ thanks to Enrico - Add: `Italian translation <https://git.ziirish.me/ziirish/burp-ui/merge_requests/74>`_ thanks to Enrico
- Add: new `client configuration templates <https://git.ziirish.me/ziirish/burp-ui/issues/155>`_
- Add: `backups deletion <https://git.ziirish.me/ziirish/burp-ui/issues/203>`_ - Add: `backups deletion <https://git.ziirish.me/ziirish/burp-ui/issues/203>`_
- Add: `show last client status in client view <https://git.ziirish.me/ziirish/burp-ui/issues/212>`_ - Add: `show last client status in client view <https://git.ziirish.me/ziirish/burp-ui/issues/212>`_
- Add: `record login failure attempt <https://git.ziirish.me/ziirish/burp-ui/issues/214>`_ - Add: `record login failure attempt <https://git.ziirish.me/ziirish/burp-ui/issues/214>`_

View file

@ -17,7 +17,7 @@ from ..utils import NOTIF_INFO
from six import iteritems from six import iteritems
from flask_babel import gettext as _, refresh from flask_babel import gettext as _, refresh
from flask import jsonify, request, url_for, current_app from flask import jsonify, request, url_for, current_app, g
from ..datastructures import ImmutableMultiDict from ..datastructures import ImmutableMultiDict
bui = current_app # type: BUIServer bui = current_app # type: BUIServer
@ -236,21 +236,40 @@ class ServerSettings(Resource):
res = bui.client.read_conf_srv(conf, server) res = bui.client.read_conf_srv(conf, server)
refresh() refresh()
# Translate the doc and placeholder API side # Translate the doc and placeholder API side
doc = bui.client.get_parser_attr('doc', server).copy() cache_keys = {
placeholders = bui.client.get_parser_attr('placeholders', server).copy() 'doc': '_doc_parser_{}-{}'.format(server, g.locale),
for key, val in iteritems(doc): 'placeholders': '_placeholders_parser_{}-{}'.format(server, g.locale),
doc[key] = _(val) 'boolean_srv': '_boolean_srv_parser_{}'.format(server),
for key, val in iteritems(placeholders): 'string_srv': '_string_srv_parser_{}'.format(server),
placeholders[key] = _(val) 'integer_srv': '_integer_srv_parser_{}'.format(server),
'multi_srv': '_multi_srv_parser_{}'.format(server),
'values': '_suggest_parser_{}'.format(server),
'defaults': '_defaults_parser_{}'.format(server),
}
cache_results = {}
for name, key in iteritems(cache_keys):
if not cache.cache.has(key):
if name in ['doc', 'placeholders']:
_tmp = bui.client.get_parser_attr(name, server).copy()
_tmp2 = {}
for k, v in iteritems(_tmp):
_tmp2[k] = _(v)
cache_results[name] = _tmp2
else:
cache_results[name] = bui.client.get_parser_attr(name, server)
cache.cache.set(key, cache_results[name], 3600)
else:
cache_results[name] = cache.cache.get(key)
return jsonify(results=res, return jsonify(results=res,
boolean=bui.client.get_parser_attr('boolean_srv', server), boolean=cache_results['boolean_srv'],
string=bui.client.get_parser_attr('string_srv', server), string=cache_results['string_srv'],
integer=bui.client.get_parser_attr('integer_srv', server), integer=cache_results['integer_srv'],
multi=bui.client.get_parser_attr('multi_srv', server), multi=cache_results['multi_srv'],
server_doc=doc, server_doc=cache_results['doc'],
suggest=bui.client.get_parser_attr('values', server), suggest=cache_results['values'],
placeholders=placeholders, placeholders=cache_results['placeholders'],
defaults=bui.client.get_parser_attr('defaults', server)) defaults=cache_results['defaults'])
@ns.route('/clients', @ns.route('/clients',
@ -273,10 +292,84 @@ class ClientsList(Resource):
) )
def get(self, server=None): def get(self, server=None):
"""Returns a list of clients""" """Returns a list of clients"""
res = bui.client.clients_list(server) parser = bui.client.get_parser(agent=server)
res = parser.list_clients()
return jsonify(result=res) return jsonify(result=res)
@ns.route('/templates',
'/<server>/templates',
endpoint='templates_list')
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
},
)
class TemplatesList(Resource):
@api.acl_admin_required(message='Sorry, you don\'t have rights to access the setting panel')
@ns.doc(
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def get(self, server=None):
"""Returns a list of clients"""
parser = bui.client.get_parser(agent=server)
res = parser.list_templates()
return jsonify(result=res)
@ns.route('/template',
'/<server>/template',
endpoint='new_template',
methods=['PUT'])
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
},
)
class NewTemplateSettings(Resource):
parser = ns.parser()
parser.add_argument('newtemplate', required=True, help="No 'newclient' provided")
@api.disabled_on_demo()
@api.acl_admin_required(message='Sorry, you don\'t have rights to access the setting panel')
@ns.expect(parser)
@ns.doc(
responses={
200: 'Success',
400: 'Missing parameter',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def put(self, server=None):
"""Creates a new client"""
newtemplate = self.parser.parse_args()['newtemplate']
if not newtemplate:
self.abort(400, 'No template name provided')
parser = bui.client.get_parser(agent=server)
templates = parser.list_templates()
for tpl in templates:
if tpl['name'] == newtemplate:
self.abort(409, "Template '{}' already exists".format(newtemplate))
# clientconfdir = bui.client.get_parser_attr('clientconfdir', server)
# if not clientconfdir:
# flash('Could not proceed, no \'clientconfdir\' find', 'warning')
# return redirect(request.referrer)
noti = bui.client.store_conf_cli(ImmutableMultiDict(), newtemplate, None, True, server)
if server:
noti.append([NOTIF_INFO, _('<a href="%(url)s">Click here</a> to edit \'%(template)s\' configuration', url=url_for('view.cli_settings', server=server, client=newtemplate, template=True), template=newtemplate)])
else:
noti.append([NOTIF_INFO, _('<a href="%(url)s">Click here</a> to edit \'%(template)s\' configuration', url=url_for('view.cli_settings', client=newtemplate, template=True), template=newtemplate)])
# clear the cache when we add a new client
cache.clear()
return {'notif': noti}, 201
@ns.route('/config', @ns.route('/config',
'/<server>/config', '/<server>/config',
endpoint='new_client', endpoint='new_client',
@ -306,7 +399,8 @@ class NewClientSettings(Resource):
newclient = self.parser.parse_args()['newclient'] newclient = self.parser.parse_args()['newclient']
if not newclient: if not newclient:
self.abort(400, 'No client name provided') self.abort(400, 'No client name provided')
clients = bui.client.clients_list(server) parser = bui.client.get_parser(agent=server)
clients = parser.list_clients()
for cl in clients: for cl in clients:
if cl['name'] == newclient: if cl['name'] == newclient:
self.abort(409, "Client '{}' already exists".format(newclient)) self.abort(409, "Client '{}' already exists".format(newclient))
@ -345,9 +439,15 @@ class ClientSettings(Resource):
parser_delete.add_argument('revoke', type=boolean, help='Whether to revoke the certificate or not', default=False, nullable=True) parser_delete.add_argument('revoke', type=boolean, help='Whether to revoke the certificate or not', default=False, nullable=True)
parser_delete.add_argument('delcert', type=boolean, help='Whether to delete the certificate or not', default=False, nullable=True) parser_delete.add_argument('delcert', type=boolean, help='Whether to delete the certificate or not', default=False, nullable=True)
parser_delete.add_argument('keepconf', type=boolean, help='Whether to keep the conf or not', default=False, nullable=True) parser_delete.add_argument('keepconf', type=boolean, help='Whether to keep the conf or not', default=False, nullable=True)
parser_delete.add_argument('template', type=boolean, help='Whether we work on a template or not', default=False, nullable=True)
parser_post = ns.parser()
parser_post.add_argument('template', type=boolean, help='Whether we work on a template or not', default=False, nullable=True)
parser_get = ns.parser()
parser_get.add_argument('template', type=boolean, help='Whether we work on a template or not', default=False, nullable=True)
@api.disabled_on_demo() @api.disabled_on_demo()
@api.acl_admin_required(message=_('Sorry, you don\'t have rights to access the setting panel')) @api.acl_admin_required(message=_('Sorry, you don\'t have rights to access the setting panel'))
@ns.expect(parser_post)
@ns.doc( @ns.doc(
responses={ responses={
200: 'Success', 200: 'Success',
@ -357,10 +457,13 @@ class ClientSettings(Resource):
) )
def post(self, server=None, client=None, conf=None): def post(self, server=None, client=None, conf=None):
"""Saves a given client configuration""" """Saves a given client configuration"""
noti = bui.client.store_conf_cli(request.form, client, conf, server) args = self.parser_post.parse_args()
template = args.get('template', False)
noti = bui.client.store_conf_cli(request.form, client, conf, template, server)
return {'notif': noti} return {'notif': noti}
@api.acl_admin_required(message=_('Sorry, you don\'t have rights to access the setting panel')) @api.acl_admin_required(message=_('Sorry, you don\'t have rights to access the setting panel'))
@ns.expect(parser_get)
@ns.doc( @ns.doc(
responses={ responses={
200: 'Success', 200: 'Success',
@ -374,25 +477,47 @@ class ClientSettings(Resource):
conf = unquote(conf) conf = unquote(conf)
except: except:
pass pass
res = bui.client.read_conf_cli(client, conf, server) args = self.parser_get.parse_args()
template = args.get('template', False)
parser = bui.client.get_parser()
res = parser.read_client_conf(client, conf, template)
refresh() refresh()
# Translate the doc and placeholder API side # Translate the doc and placeholder API side
doc = bui.client.get_parser_attr('doc', server).copy() cache_keys = {
placeholders = bui.client.get_parser_attr('placeholders', server).copy() 'doc': '_doc_parser_{}-{}'.format(server, g.locale),
for key, val in iteritems(doc): 'placeholders': '_placeholders_parser_{}-{}'.format(server, g.locale),
doc[key] = _(val) 'boolean_cli': '_boolean_cli_parser_{}'.format(server),
for key, val in iteritems(placeholders): 'string_cli': '_string_cli_parser_{}'.format(server),
placeholders[key] = _(val) 'integer_cli': '_integer_cli_parser_{}'.format(server),
'multi_cli': '_multi_cli_parser_{}'.format(server),
'values': '_suggest_parser_{}'.format(server),
'defaults': '_defaults_parser_{}'.format(server),
}
cache_results = {}
for name, key in iteritems(cache_keys):
if not cache.cache.has(key):
if name in ['doc', 'placeholders']:
_tmp = bui.client.get_parser_attr(name, server).copy()
_tmp2 = {}
for k, v in iteritems(_tmp):
_tmp2[k] = _(v)
cache_results[name] = _tmp2
else:
cache_results[name] = bui.client.get_parser_attr(name, server)
cache.cache.set(key, cache_results[name], 3600)
else:
cache_results[name] = cache.cache.get(key)
return jsonify( return jsonify(
results=res, results=res,
boolean=bui.client.get_parser_attr('boolean_cli', server), boolean=cache_results['boolean_cli'],
string=bui.client.get_parser_attr('string_cli', server), string=cache_results['string_cli'],
integer=bui.client.get_parser_attr('integer_cli', server), integer=cache_results['integer_cli'],
multi=bui.client.get_parser_attr('multi_cli', server), multi=cache_results['multi_cli'],
server_doc=doc, server_doc=cache_results['doc'],
suggest=bui.client.get_parser_attr('values', server), suggest=cache_results['values'],
placeholders=placeholders, placeholders=cache_results['placeholders'],
defaults=bui.client.get_parser_attr('defaults', server) defaults=cache_results['defaults']
) )
@api.disabled_on_demo() @api.disabled_on_demo()
@ -411,6 +536,7 @@ class ClientSettings(Resource):
delcert = args.get('delcert', False) delcert = args.get('delcert', False)
revoke = args.get('revoke', False) revoke = args.get('revoke', False)
keepconf = args.get('keepconf', False) keepconf = args.get('keepconf', False)
template = args.get('template', False)
if not keepconf: if not keepconf:
# clear the cache when we remove a client # clear the cache when we remove a client
@ -418,7 +544,8 @@ class ClientSettings(Resource):
if bui.config['WITH_CELERY']: if bui.config['WITH_CELERY']:
from ..tasks import force_scheduling_now from ..tasks import force_scheduling_now
force_scheduling_now() force_scheduling_now()
return bui.client.delete_client(client, keepconf=keepconf, delcert=delcert, revoke=revoke, agent=server), 200 parser = bui.client.get_parser()
return parser.remove_client(client, keepconf, delcert, revoke, template), 200
@ns.route('/path-expander', @ns.route('/path-expander',
@ -459,7 +586,8 @@ class PathExpander(Resource):
path = unquote(path) path = unquote(path)
if source: if source:
source = unquote(source) source = unquote(source)
paths = bui.client.expand_path(path, source, client, server) parser = bui.client.get_parser(agent=server)
paths = parser.path_expander(path, source, client)
if not paths: if not paths:
self.abort(403, 'Path not found') self.abort(403, 'Path not found')
return {'result': paths} return {'result': paths}

View file

@ -1000,7 +1000,7 @@ class Burp(BUIbackend):
return [] return []
return self.parser.read_server_conf(conf) return self.parser.read_server_conf(conf)
def store_conf_cli(self, data, client=None, conf=None, agent=None): def store_conf_cli(self, data, client=None, conf=None, template=False, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`""" """See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`"""
if not self.parser: if not self.parser:
return [] return []
@ -1008,7 +1008,7 @@ class Burp(BUIbackend):
conf = unquote(conf) conf = unquote(conf)
except: except:
pass pass
return self.parser.store_client_conf(data, client, conf) return self.parser.store_client_conf(data, client, conf, template)
def store_conf_srv(self, data, conf=None, agent=None): def store_conf_srv(self, data, conf=None, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv`""" """See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv`"""

View file

@ -806,7 +806,7 @@ class BUIbackend(with_metaclass(ABCMeta, object)):
raise NotImplementedError("Sorry, the current Backend does not implement this method!") # pragma: no cover raise NotImplementedError("Sorry, the current Backend does not implement this method!") # pragma: no cover
@abstractmethod @abstractmethod
def store_conf_cli(self, data, client=None, conf=None, agent=None): def store_conf_cli(self, data, client=None, conf=None, template=False, agent=None):
"""The :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli` """The :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`
function works the same way as the function works the same way as the
:func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv` function :func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv` function

View file

@ -511,7 +511,7 @@ class NClient(BUIbackend):
""" """
@implement @implement
def store_conf_cli(self, data, client=None, conf=None, agent=None): def store_conf_cli(self, data, client=None, conf=None, template=False, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`""" """See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`"""
# serialize data as it is a nested dict # serialize data as it is a nested dict
import hmac import hmac
@ -527,7 +527,7 @@ class NClient(BUIbackend):
data = ImmutableMultiDict(data.to_dict(False)) data = ImmutableMultiDict(data.to_dict(False))
key = '{}{}'.format(self.password, 'store_conf_cli') key = '{}{}'.format(self.password, 'store_conf_cli')
key = to_bytes(key) key = to_bytes(key)
pickles = b64encode(pickle.dumps({'data': data, 'conf': conf, 'client': client}, 2)) pickles = b64encode(pickle.dumps({'data': data, 'conf': conf, 'client': client, 'template': template}, 2))
bytes_pickles = to_bytes(pickles) bytes_pickles = to_bytes(pickles)
digest = hmac.new(key, bytes_pickles, hashlib.sha1).hexdigest() digest = hmac.new(key, bytes_pickles, hashlib.sha1).hexdigest()
data = {'func': 'store_conf_cli', 'args': pickles, 'pickled': True, 'digest': digest} data = {'func': 'store_conf_cli', 'args': pickles, 'pickled': True, 'digest': digest}

View file

@ -41,8 +41,12 @@ class Parser(Doc):
self._server_conf = {} self._server_conf = {}
self._client_conf = {} self._client_conf = {}
self._clients_conf = {} self._clients_conf = {}
self._templates_conf = {}
self.clientconfdir = None self.clientconfdir = None
self.clientconfdir_mtime = None self.clientconfdir_mtime = None
self.templates = []
self.templates_dir = '.buitemplates'
self.templates_path = None
self.filescache = {} self.filescache = {}
self._configs = {} self._configs = {}
self.root = None self.root = None
@ -89,6 +93,7 @@ class Parser(Doc):
self._server_conf.clear() self._server_conf.clear()
self._client_conf.clear() self._client_conf.clear()
self._clients_conf.clear() self._clients_conf.clear()
self._list_templates(True)
self._list_clients(True) self._list_clients(True)
def _load_conf_srv(self): def _load_conf_srv(self):
@ -96,6 +101,7 @@ class Parser(Doc):
self._server_conf = Config(self.conf, self, 'srv') self._server_conf = Config(self.conf, self, 'srv')
self._server_conf.parse() self._server_conf.parse()
self.clientconfdir = self._server_conf.get('clientconfdir') self.clientconfdir = self._server_conf.get('clientconfdir')
self.templates_path = os.path.join(self.clientconfdir, self.templates_dir)
def _load_conf_cli(self): def _load_conf_cli(self):
"""Load the client configuration file""" """Load the client configuration file"""
@ -120,11 +126,26 @@ class Parser(Doc):
conf.parse() conf.parse()
self._clients_conf[cli['name']] = conf self._clients_conf[cli['name']] = conf
def _load_conf_templates(self):
"""Load all templates configuration"""
templates = self._list_templates(True)
for template in templates:
conf = self.server_conf.clone()
path = os.path.join(self.templates_path, template['name'])
if template['name'] not in self._templates_conf:
conf.add_file(path)
conf.set_default(path)
conf.parse()
self._templates_conf[template['name']] = conf
def _load_all_conf(self): def _load_all_conf(self):
"""Load all configurations""" """Load all configurations"""
self._load_conf_srv() self._load_conf_srv()
self._load_conf_cli() self._load_conf_cli()
self._load_conf_clients() self._load_conf_clients()
self._load_conf_templates()
def _new_client_conf(self, name, path): def _new_client_conf(self, name, path):
"""Create new client conf""" """Create new client conf"""
@ -153,6 +174,17 @@ class Parser(Doc):
self._clients_conf[name].parse() self._clients_conf[name].parse()
return self._clients_conf[name] return self._clients_conf[name]
def _get_template(self, name):
"""Return template conf and refresh it if necessary"""
if self._clientconfdir_changed() and name not in self._templates_conf:
self._templates_conf.clear()
self._load_conf_templates()
if name not in self._templates_conf:
return None
if self._templates_conf[name].changed:
self._templates_conf[name].parse()
return self._templates_conf[name]
def _get_config(self, path, mode='cli'): def _get_config(self, path, mode='cli'):
"""Return conf by it's path""" """Return conf by it's path"""
if path in self._configs: if path in self._configs:
@ -200,6 +232,27 @@ class Parser(Doc):
self.clientconfdir_mtime = os.path.getmtime(self.clientconfdir) self.clientconfdir_mtime = os.path.getmtime(self.clientconfdir)
return res return res
def _list_templates(self, force=False):
if not self.clientconfdir:
return []
if self.templates and not force and not self._clientconfdir_changed():
return self.templates
res = []
for tpl in os.listdir(self.templates_path):
full_file = os.path.join(self.templates_path, tpl)
if (os.path.isfile(full_file) and not tpl.startswith('.') and
not tpl.endswith('~')):
res.append({
'name': tpl,
'value': os.path.join(self.templates_dir, tpl)
})
self.templates = res
self.clientconfdir_mtime = os.path.getmtime(self.clientconfdir)
return res
def _get_server_path(self, name=None, fil=None): def _get_server_path(self, name=None, fil=None):
"""Returns the path of the 'server *fil*' file""" """Returns the path of the 'server *fil*' file"""
if not name: if not name:
@ -256,7 +309,7 @@ class Parser(Doc):
return False return False
return self.openssl_auth.check_client_revoked(client) return self.openssl_auth.check_client_revoked(client)
def remove_client(self, client=None, keepconf=False, delcert=False, revoke=False): def remove_client(self, client=None, keepconf=False, delcert=False, revoke=False, template=False):
"""See :func:`burpui.misc.parser.interface.BUIparser.remove_client`""" """See :func:`burpui.misc.parser.interface.BUIparser.remove_client`"""
res = [] res = []
revoked = False revoked = False
@ -265,12 +318,15 @@ class Parser(Doc):
return [[NOTIF_ERROR, "No client provided"]] return [[NOTIF_ERROR, "No client provided"]]
try: try:
if not keepconf: if not keepconf:
path = os.path.join(self.clientconfdir, client) if template:
path = os.path.join(self.templates_path, client)
else:
path = os.path.join(self.clientconfdir, client)
os.unlink(path) os.unlink(path)
res.append([NOTIF_OK, "'{}' successfully removed".format(client)]) res.append([NOTIF_OK, "'{}' successfully removed".format(client)])
removed = True removed = True
if client in self._clients_conf: if client in self._clients_conf and not template:
del self._clients_conf[client] del self._clients_conf[client]
self._refresh_cache() self._refresh_cache()
@ -302,7 +358,7 @@ class Parser(Doc):
return res return res
def read_client_conf(self, client=None, conf=None): def read_client_conf(self, client=None, conf=None, template=False):
""" """
See :func:`burpui.misc.parser.interface.BUIparser.read_client_conf` See :func:`burpui.misc.parser.interface.BUIparser.read_client_conf`
""" """
@ -313,7 +369,7 @@ class Parser(Doc):
u'multi': [], u'multi': [],
u'includes': [], u'includes': [],
u'includes_ext': [], u'includes_ext': [],
u'clients': self._list_clients(), u'templates': [],
u'hierarchy': [], u'hierarchy': [],
} }
if not client and not conf: if not client and not conf:
@ -323,8 +379,12 @@ class Parser(Doc):
if not mconf: if not mconf:
if not self.clientconfdir: if not self.clientconfdir:
return res return res
mconf = os.path.join(self.clientconfdir, client) if template:
config = self._get_client(client, mconf) mconf = os.path.join(self.templates_path, client)
config = self._get_template(client)
else:
mconf = os.path.join(self.clientconfdir, client)
config = self._get_client(client, mconf)
else: else:
config = self._get_config(mconf) config = self._get_config(mconf)
@ -337,6 +397,7 @@ class Parser(Doc):
res2[u'boolean'] = parsed.boolean res2[u'boolean'] = parsed.boolean
res2[u'integer'] = parsed.integer res2[u'integer'] = parsed.integer
res2[u'multi'] = parsed.multi res2[u'multi'] = parsed.multi
res2[u'templates'] = parsed.template
res2[u'includes'] = [ res2[u'includes'] = [
x x
for x in parsed.flatten('include', False).keys() for x in parsed.flatten('include', False).keys()
@ -363,7 +424,6 @@ class Parser(Doc):
u'multi': [], u'multi': [],
u'includes': [], u'includes': [],
u'includes_ext': [], u'includes_ext': [],
u'clients': self._list_clients(),
u'hierarchy': [], u'hierarchy': [],
} }
if not conf: if not conf:
@ -378,10 +438,6 @@ class Parser(Doc):
return self.filescache[mconf]['dict'] return self.filescache[mconf]['dict']
clientconfdir = parsed.get('clientconfdir') clientconfdir = parsed.get('clientconfdir')
if clientconfdir and clientconfdir.parse() != self.clientconfdir:
self.clientconfdir = clientconfdir.parse()
self.clientconfdir_mtime = -1
res['clients'] = self._list_clients()
res2 = {} res2 = {}
res2[u'common'] = parsed.string res2[u'common'] = parsed.string
@ -405,34 +461,43 @@ class Parser(Doc):
def list_clients(self): def list_clients(self):
"""See :func:`burpui.misc.parser.interface.BUIparser.list_clients`""" """See :func:`burpui.misc.parser.interface.BUIparser.list_clients`"""
self.read_server_conf() self.read_server_conf()
if not self.clientconfdir:
return []
return self._list_clients() return self._list_clients()
def store_client_conf(self, data, client=None, conf=None): def list_templates(self):
"""See :func:`burpui.misc.parser.interface.BUIparser.list_templates`"""
self.read_server_conf()
return self._list_templates()
def store_client_conf(self, data, client=None, conf=None, template=False):
""" """
See :func:`burpui.misc.parser.interface.BUIparser.store_client_conf` See :func:`burpui.misc.parser.interface.BUIparser.store_client_conf`
""" """
if conf and not os.path.isabs(conf): if conf and not os.path.isabs(conf):
conf = os.path.join(self.clientconfdir, conf) conf = os.path.join(self.clientconfdir, conf)
if not conf and not client: if not conf and not client:
if template:
return [[NOTIF_ERROR, 'Sorry, no template defined']]
return [[NOTIF_ERROR, 'Sorry, no client defined']] return [[NOTIF_ERROR, 'Sorry, no client defined']]
elif client and not conf: elif client and not conf:
conf = os.path.join(self.clientconfdir, client) if template:
if not self.templates_path:
return [[NOTIF_ERROR, 'Sorry, no template directory found']]
conf = os.path.join(self.templates_path, client)
else:
conf = os.path.join(self.clientconfdir, client)
ret = self.store_conf(data, conf, client, mode='cli') ret = self.store_conf(data, conf, client, mode='cli')
self._refresh_cache() # refresh client list self._refresh_cache() # refresh client list
return ret return ret
def store_conf(self, data, conf=None, client=None, mode='srv', def store_conf(self, data, conf=None, client=None, mode='srv',
insecure=False): insecure=False, template=False):
"""See :func:`burpui.misc.parser.interface.BUIparser.store_conf`""" """See :func:`burpui.misc.parser.interface.BUIparser.store_conf`"""
mconf = None mconf = None
if not conf: if not conf:
mconf = self.conf mconf = self.conf
else: else:
mconf = conf mconf = conf
if mconf != self.conf and not mconf.startswith('/'): if mconf != self.conf and not os.path.isabs(mconf):
mconf = os.path.join(self.root, mconf) mconf = os.path.join(self.root, mconf)
if not mconf: if not mconf:
return [[NOTIF_WARN, 'Sorry, no configuration file defined']] return [[NOTIF_WARN, 'Sorry, no configuration file defined']]
@ -447,7 +512,9 @@ class Parser(Doc):
] ]
check = False check = False
if client: if template:
conffile = self._get_template(client, mconf).get_file(mconf)
elif client:
conffile = self._get_client(client, mconf).get_file(mconf) conffile = self._get_client(client, mconf).get_file(mconf)
else: else:
conffile = self.server_conf.get_file(mconf) conffile = self.server_conf.get_file(mconf)

View file

@ -7,7 +7,7 @@
.. moduleauthor:: Ziirish <hi+burpui@ziirish.me> .. moduleauthor:: Ziirish <hi+burpui@ziirish.me>
""" """
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod, abstractproperty
from six import with_metaclass from six import with_metaclass
import logging import logging
@ -134,7 +134,7 @@ class BUIparser(with_metaclass(ABCMeta, object)):
) # pragma: no cover ) # pragma: no cover
@abstractmethod @abstractmethod
def store_client_conf(self, data, client=None, conf=None): def store_client_conf(self, data, client=None, conf=None, template=False):
""":func:`burpui.misc.parser.interface.BUIparser.store_client_conf` is """:func:`burpui.misc.parser.interface.BUIparser.store_client_conf` is
used by :func:`burpui.misc.backend.BUIbackend.store_conf_cli`. used by :func:`burpui.misc.backend.BUIbackend.store_conf_cli`.
@ -144,6 +144,12 @@ class BUIparser(with_metaclass(ABCMeta, object)):
:param client: Name of the client for which to apply this config :param client: Name of the client for which to apply this config
:type client: str :type client: str
:param conf: The explicit filename of the conf
:type conf: str
:param template: Is this file a template
:type template: bool
""" """
raise NotImplementedError( raise NotImplementedError(
"Sorry, the current Parser does not implement this method!" "Sorry, the current Parser does not implement this method!"
@ -151,7 +157,7 @@ class BUIparser(with_metaclass(ABCMeta, object)):
@abstractmethod @abstractmethod
def store_conf(self, data, conf=None, client=None, mode='srv', def store_conf(self, data, conf=None, client=None, mode='srv',
insecure=False): insecure=False, template=False):
""":func:`burpui.misc.parser.interface.BUIparser.store_conf` is used to """:func:`burpui.misc.parser.interface.BUIparser.store_conf` is used to
store the configuration from the web-ui into the actual configuration store the configuration from the web-ui into the actual configuration
files. files.
@ -173,6 +179,9 @@ class BUIparser(with_metaclass(ABCMeta, object)):
:param insecure: Used for the CLI :param insecure: Used for the CLI
:type insecure: bool :type insecure: bool
:param template: Is it a template
:type template: bool
:returns: A list of notifications to return to the UI (success or :returns: A list of notifications to return to the UI (success or
failure) failure)
@ -216,6 +225,17 @@ class BUIparser(with_metaclass(ABCMeta, object)):
"Sorry, the current Parser does not implement this method!" "Sorry, the current Parser does not implement this method!"
) # pragma: no cover ) # pragma: no cover
@abstractmethod
def list_templates(self):
""":func:`burpui.misc.parser.interface.BUIparser.list_templates` is used
to retrieve a list of templates with their absolute paths.
:returns: A list of templates
"""
raise NotImplementedError(
"Sorry, the current Parser does not implement this method!"
) # pragma: no cover
@abstractmethod @abstractmethod
def is_client_revoked(self, client=None): def is_client_revoked(self, client=None):
""":func:`burpui.misc.parser.interface.BUIparser.is_client_revoked` is """:func:`burpui.misc.parser.interface.BUIparser.is_client_revoked` is
@ -231,7 +251,7 @@ class BUIparser(with_metaclass(ABCMeta, object)):
) # pragma: no cover ) # pragma: no cover
@abstractmethod @abstractmethod
def remove_client(self, client=None, keepconf=False, delcert=False, revoke=False): def remove_client(self, client=None, keepconf=False, delcert=False, revoke=False, template=False):
""":func:`burpui.misc.parser.interface.BUIparser.remove_client` is used """:func:`burpui.misc.parser.interface.BUIparser.remove_client` is used
to delete a client from burp's configuration. to delete a client from burp's configuration.
@ -247,6 +267,9 @@ class BUIparser(with_metaclass(ABCMeta, object)):
:param revoke: Whether to revoke the associated certificate :param revoke: Whether to revoke the associated certificate
:type revoke: bool :type revoke: bool
:param template: Whether we remove a template
:type template: bool
:returns: A list of notifications to return to the UI (success or :returns: A list of notifications to return to the UI (success or
failure) failure)
""" """
@ -255,7 +278,7 @@ class BUIparser(with_metaclass(ABCMeta, object)):
) # pragma: no cover ) # pragma: no cover
@abstractmethod @abstractmethod
def read_client_conf(self, client=None, conf=None): def read_client_conf(self, client=None, conf=None, template=False):
""":func:`burpui.misc.parser.interface.BUIparser.read_client_conf` is """:func:`burpui.misc.parser.interface.BUIparser.read_client_conf` is
called by :func:`burpui.misc.backend.interface.BUIbackend.read_conf_cli` called by :func:`burpui.misc.backend.interface.BUIbackend.read_conf_cli`
in order to parse the burp-clients configuration files. in order to parse the burp-clients configuration files.

View file

@ -23,6 +23,8 @@ from ...datastructures import MultiDict
RESET_IDENTIFIER = '_reset_bui_CUSTOM' RESET_IDENTIFIER = '_reset_bui_CUSTOM'
BEGIN_TEMPLATES = 'BURP-UI TEMPLATES'
END_TEMPLATES = 'END TEMPLATES'
class Option(object): class Option(object):
@ -272,7 +274,7 @@ class OptionInc(Option):
def _path_absolute(self, path): def _path_absolute(self, path):
absolute = path absolute = path
if not path.startswith('/'): if not os.path.isabs(path):
if self.root: if self.root:
absolute = os.path.join(self.root, path) absolute = os.path.join(self.root, path)
elif self.mode == 'srv': elif self.mode == 'srv':
@ -321,6 +323,60 @@ class OptionInc(Option):
return '' return ''
class OptionTpl(Option):
"""Option type Template
Example:
. .buitemplates/windows
"""
type = 'template'
delim = ""
def __init__(self, parser, name, value=None):
"""
:param parser: Parser instance
:type parser: :class:`burpui.misc.parser.burp1.Parser`
"""
super(OptionTpl, self).__init__(name, value)
self.parser = parser
self.extended = False
self._dirty = True
if name:
self._id = name.split(os.path.sep)[-1]
else:
self._id = ''
@property
def dirty(self):
return self._dirty
def _path_absolute(self, path):
absolute = path
if not os.path.isabs(path):
absolute = os.path.join(self.parser.clientconfdir, path)
return absolute
def extend(self):
"""Helper function for the parsing"""
if not self._dirty and self.extended:
return self.extended
path = self._path_absolute(self.value)
self.clean()
self.extended = path
return path
def parse(self):
"""Parse the option value"""
return self.extend()
def dump(self):
"""Return the option representation to store in configuration file"""
if self.extend() and not self.parser.backend.enforce:
return '. {}'.format(self.name)
# if the include did not match anything, we can safely remove it
return ''
class File(dict): class File(dict):
"""Object representing a configuration file """Object representing a configuration file
@ -345,6 +401,8 @@ class File(dict):
self._dirty = False self._dirty = False
# _changed is used to know if the file changed since last read # _changed is used to know if the file changed since last read
self._changed = True self._changed = True
# _parsing_templates is used to know if we are currently parsing templates
self._parsing_templates = False
# cache the content of the file # cache the content of the file
self._raw = [] self._raw = []
self._raw_data = MultiDict() self._raw_data = MultiDict()
@ -362,6 +420,7 @@ class File(dict):
'include': OrderedDict(), 'include': OrderedDict(),
'multi': OrderedDict(), 'multi': OrderedDict(),
'string': OrderedDict(), 'string': OrderedDict(),
'template': OrderedDict(),
} }
if self.name: if self.name:
self.parse() self.parse()
@ -372,6 +431,10 @@ class File(dict):
if val.dirty: if val.dirty:
self._changed = True self._changed = True
return self._changed return self._changed
for key, val in iteritems(self.types['template']):
if val.dirty:
self._changed = True
return self._changed
try: try:
if self.name: if self.name:
mtime = os.path.getmtime(self.name) mtime = os.path.getmtime(self.name)
@ -463,6 +526,16 @@ class File(dict):
def include(self): def include(self):
return self.flatten('include') return self.flatten('include')
@property
def template(self):
ret = []
for tpl in self.flatten('template', parse=False):
ret.append({
'value': tpl['name'],
'name': tpl['value']._id,
})
return ret
@property @property
def multi(self): def multi(self):
return self.flatten('multi') return self.flatten('multi')
@ -506,7 +579,7 @@ class File(dict):
return opt return opt
if typ == 'include': if typ == 'include':
key = value key = value
opt = OptionInc( return OptionInc(
self.parser, self.parser,
key, key,
value, value,
@ -548,13 +621,16 @@ class File(dict):
opt.append(value) opt.append(value)
elif key == u'.': elif key == u'.':
key = value key = value
opt = OptionInc( if self._parsing_templates:
self.parser, opt = OptionTpl(self.parser, key, value)
key, else:
value, opt = OptionInc(
root=self.name, self.parser,
mode=self.mode key,
) value,
root=self.name,
mode=self.mode
)
else: else:
opt = OptionStr(key, value) opt = OptionStr(key, value)
self.options[key] = opt self.options[key] = opt
@ -698,6 +774,10 @@ class File(dict):
self.clear() self.clear()
for line in self.raw: for line in self.raw:
if re.match(r'^\s*#', line): if re.match(r'^\s*#', line):
if BEGIN_TEMPLATES in line:
self._parsing_templates = True
if END_TEMPLATES in line:
self._parsing_templates = False
continue continue
res = re.search(r'\s*([^=\s]+)\s*(:)?=?\s*(.*)$', line) res = re.search(r'\s*([^=\s]+)\s*(:)?=?\s*(.*)$', line)
if res: if res:
@ -821,7 +901,21 @@ class File(dict):
with codecs.open(dest, 'w', 'utf-8', errors='ignore') as fil: with codecs.open(dest, 'w', 'utf-8', errors='ignore') as fil:
# f.write('# Auto-generated configuration using Burp-UI\n') # f.write('# Auto-generated configuration using Burp-UI\n')
data_keys = list(data.keys()) data_keys = list(data.keys())
if len(self.template) > 0 or 'templates' in data:
_dump(' {}'.format(BEGIN_TEMPLATES), True)
tpls = data.getlist('templates') or [x['value'] for x in self.template]
for tpl in tpls:
self._write_key(fil, '.', tpl)
_dump(' {}'.format(END_TEMPLATES), True)
skip_line = False
for idx, line in enumerate(orig): for idx, line in enumerate(orig):
if self._line_is_comment(line) and BEGIN_TEMPLATES in line:
skip_line = True
if self._line_is_comment(line) and END_TEMPLATES in line:
skip_line = False
continue
if skip_line:
continue
key = self._get_line_key(line, False) key = self._get_line_key(line, False)
if (self._line_removed(line, data_keys) and if (self._line_removed(line, data_keys) and
not self._line_is_comment(line) and not self._line_is_comment(line) and
@ -917,7 +1011,7 @@ class File(dict):
if key.endswith(RESET_IDENTIFIER): if key.endswith(RESET_IDENTIFIER):
continue continue
if (key not in written and key not in already_multi and if (key not in written and key not in already_multi and
key not in ['includes', 'includes_ori']): key not in ['includes', 'includes_ori', 'templates']):
self._write_key( self._write_key(
fil, fil,
key, key,
@ -1016,6 +1110,7 @@ class Config(File):
self.default = path self.default = path
self.name = path self.name = path
self._includes = [] self._includes = []
self._templates = []
self._dirty = True self._dirty = True
if path: if path:
self.files[path] = File(parser, path, mode=mode) self.files[path] = File(parser, path, mode=mode)
@ -1039,6 +1134,11 @@ class Config(File):
path = os.path.join(os.path.dirname(root), path) path = os.path.join(os.path.dirname(root), path)
self.add_file(path, root) self.add_file(path, root)
self._includes.append(path) self._includes.append(path)
for key, path in iteritems(conf.flatten('template', False)):
if not os.path.isabs(path):
path = os.path.join(os.path.dirname(root), path)
self.add_file(path, root)
self._templates.append(path)
# recursively parse the conf # recursively parse the conf
if orig != self.files: if orig != self.files:
@ -1049,13 +1149,14 @@ class Config(File):
return return
del self._includes[:] del self._includes[:]
del self._templates[:]
self._parse() self._parse()
removed = [] removed = []
orig = self.files orig = self.files
for path, conf in iteritems(orig): for path, conf in iteritems(orig):
if conf.parent and (conf.name not in self._includes or if conf.parent and ((conf.name not in self._includes and
conf.name in removed): conf.name not in self._templates) or conf.name in removed):
removed.append(path) removed.append(path)
self.del_file(path) self.del_file(path)
@ -1110,7 +1211,7 @@ class Config(File):
if conf and conf in self.files: if conf and conf in self.files:
return self.files[conf].store(dest, insecure) return self.files[conf].store(dest, insecure)
for name, conf in iteritems(self.files): for name, conf in iteritems(self.files):
ret += conf.store(dest, insecure) ret += conf.store(insecure=insecure)
return ret return ret
def store_data(self, conf, data, insecure=False): def store_data(self, conf, data, insecure=False):

View file

@ -169,9 +169,11 @@ def cli_settings(server=None, client=None, conf=None):
pass pass
client = client or request.args.get('client') client = client or request.args.get('client')
server = server or request.args.get('serverName') server = server or request.args.get('serverName')
template = request.args.get('template') or False
return render_template( return render_template(
'settings.html', 'settings.html',
settings=True, settings=True,
template=template,
client=client, client=client,
server=server, server=server,
conf=conf, conf=conf,

View file

@ -97,6 +97,7 @@
* } * }
* The JSON is then split-ed out into several dict/arrays to build our form. * The JSON is then split-ed out into several dict/arrays to build our form.
*/ */
{% import 'macros.html' as macros %}
var app = angular.module('MainApp', ['ngSanitize', 'frapontillo.bootstrap-switch', 'ui.select', 'mgcrea.ngStrap', 'angular-onbeforeunload', 'datatables']); var app = angular.module('MainApp', ['ngSanitize', 'frapontillo.bootstrap-switch', 'ui.select', 'mgcrea.ngStrap', 'angular-onbeforeunload', 'datatables']);
@ -104,7 +105,7 @@ app.config(function(uiSelectConfig) {
uiSelectConfig.theme = 'bootstrap'; uiSelectConfig.theme = 'bootstrap';
}); });
app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope, $http, $scrollspy, DTOptionsBuilder, DTColumnDefBuilder) { app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', 'DTOptionsBuilder', 'DTColumnDefBuilder', function($scope, $http, $scrollspy, DTOptionsBuilder, DTColumnDefBuilder) {
$scope.bools = []; $scope.bools = [];
$scope.strings = []; $scope.strings = [];
$scope.clients = []; $scope.clients = [];
@ -120,63 +121,75 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
$scope.revokeEnabled = false; $scope.revokeEnabled = false;
$scope.inc_invalid = {}; $scope.inc_invalid = {};
$scope.old = {}; $scope.old = {};
$scope.raw = {};
$scope.spy = {}; $scope.spy = {};
$scope.new = { $scope.new = {
'bools': undefined, 'bools': undefined,
'integers': undefined, 'integers': undefined,
'strings': undefined, 'strings': undefined,
'multis': undefined 'multis': undefined,
'templates': undefined
}; };
$scope.add = { $scope.add = {
'bools': false, 'bools': false,
'integers': false, 'integers': false,
'strings': false, 'strings': false,
'multis': false 'multis': false,
'templates': false
}; };
$scope.changed = false; $scope.changed = false;
$scope.checkbox_translation = { $scope.checkbox_translation = {
'yes': "{{ _('yes') }}", 'yes': "{{ _('yes') }}",
'no': "{{ _('no') }}", 'no': "{{ _('no') }}",
'reset': "{{ _('Reset list') }}", 'reset': "{{ _('reset list') }}",
};
$scope.dtOptions = {
{{ macros.translate_datatable() }}
{{ macros.get_page_length() }}
}; };
$scope.dtOptions = DTOptionsBuilder.newOptions();
$scope.dtColumnDefs = [ $scope.dtColumnDefs = [
DTColumnDefBuilder.newColumnDef(0), DTColumnDefBuilder.newColumnDef(0),
DTColumnDefBuilder.newColumnDef(1), DTColumnDefBuilder.newColumnDef(1),
DTColumnDefBuilder.newColumnDef(2).notSortable(), DTColumnDefBuilder.newColumnDef(2).notSortable(),
]; ];
{% if client -%} $scope.loadConfig = function() {
$http.get('{{ url_for("api.client_settings", client=client, conf=conf, server=server) }}', { headers: { 'X-From-UI': true } }) {% if client -%}
{% else -%} {% if template -%}
$http.get('{{ url_for("api.server_settings", conf=conf, server=server) }}', { headers: { 'X-From-UI': true } }) $http.get('{{ url_for("api.client_settings", client=client, conf=conf, template=True, server=server) }}', { headers: { 'X-From-UI': true } })
{% endif -%} {% else -%}
.then(function(response) { $http.get('{{ url_for("api.client_settings", client=client, conf=conf, server=server) }}', { headers: { 'X-From-UI': true } })
data = response.data; {% endif -%}
$scope.bools = data.results.boolean; {% else -%}
$scope.all.bools = data.boolean; $http.get('{{ url_for("api.server_settings", conf=conf, server=server) }}', { headers: { 'X-From-UI': true } })
$scope.strings = data.results.common; {% endif -%}
$scope.all.strings = data.string; .then(function(response) {
$scope.integers = data.results.integer; data = response.data;
$scope.all.integers = data.integer; $scope.bools = data.results.boolean;
$scope.multis = data.results.multi; $scope.all.bools = data.boolean;
$scope.all.multis = data.multi; $scope.strings = data.results.common;
$scope.clients = data.results.clients; $scope.all.strings = data.string;
$scope.server_doc = data.server_doc; $scope.integers = data.results.integer;
$scope.suggest = data.suggest; $scope.all.integers = data.integer;
$scope.placeholders = data.placeholders; $scope.multis = data.results.multi;
$scope.defaults = data.defaults; $scope.all.multis = data.multi;
$scope.includes = data.results.includes; $scope.server_doc = data.server_doc;
$scope.includes_ori = angular.copy($scope.includes); $scope.suggest = data.suggest;
$scope.includes_ext = data.results.includes_ext; $scope.placeholders = data.placeholders;
$scope.hierarchy = data.results.hierarchy; $scope.defaults = data.defaults;
$scope.refreshHierarchy(); $scope.includes = data.results.includes;
$scope.refreshScrollspy(); $scope.includes_ori = angular.copy($scope.includes);
$('#waiting-container').hide(); $scope.includes_ext = data.results.includes_ext;
$('#settings-panel').show(); $scope.templates = data.results.templates;
}, function(response) { $scope.hierarchy = data.results.hierarchy;
notifAll(response.data); $scope.refreshHierarchy();
$('#waiting-container').hide(); $scope.refreshScrollspy();
}); $('#waiting-container').hide();
$('#settings-panel').show();
}, function(response) {
notifAll(response.data);
$('#waiting-container').hide();
});
};
$http.get('{{ url_for("api.setting_options", server=server) }}', { headers: { 'X-From-UI': true } }) $http.get('{{ url_for("api.setting_options", server=server) }}', { headers: { 'X-From-UI': true } })
.then(function(response) { .then(function(response) {
$scope.revokeEnabled = response.data.is_revocation_enabled; $scope.revokeEnabled = response.data.is_revocation_enabled;
@ -345,14 +358,24 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
$scope.new[type] = undefined; $scope.new[type] = undefined;
} }
$scope.add[type] = true; $scope.add[type] = true;
all = $scope.all[type];
if (type === 'templates') {
all = [];
_($scope.all[type]).forEach(function(value, name) {
all.push(name);
});
}
keys = _.map($scope[type], 'name'); keys = _.map($scope[type], 'name');
diff = _.difference($scope.all[type], keys); diff = _.difference(all, keys);
$scope.avail[type] = []; $scope.avail[type] = [];
_(diff).forEach(function(n) { _(diff).forEach(function(n) {
v = $scope.defaults[n]; v = $scope.defaults[n];
if (!v && type == 'multis') { if (!v && type === 'multis') {
v = ['']; v = [''];
} }
if (!v && type === 'templates') {
v = $scope.all[type][n];
}
$scope.avail[type].push({'name': n, 'value': v}); $scope.avail[type].push({'name': n, 'value': v});
}); });
}; };
@ -367,6 +390,7 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
$scope.add.multis = false; $scope.add.multis = false;
$scope.new.multis = false; $scope.new.multis = false;
$scope.changed = true; $scope.changed = true;
$scope.refreshScrollspy();
}; };
$scope.addMulti = function(pindex) { $scope.addMulti = function(pindex) {
$scope.multis[pindex].value.push(''); $scope.multis[pindex].value.push('');
@ -386,6 +410,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
$scope.old.includes_ori.push($scope.includes_ori[index]); $scope.old.includes_ori.push($scope.includes_ori[index]);
$scope.includes.splice(index, 1); $scope.includes.splice(index, 1);
$scope.includes_ori.splice(index, 1); $scope.includes_ori.splice(index, 1);
$scope.changed = true;
$scope.refreshScrollspy();
}; };
$scope.clickAddIncludes = function() { $scope.clickAddIncludes = function() {
val = ''; val = '';
@ -398,6 +424,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
} }
$scope.includes.push(val); $scope.includes.push(val);
$scope.includes_ori.push(val2); $scope.includes_ori.push(val2);
$scope.changed = true;
$scope.refreshScrollspy();
}; };
$scope.select = function(selected, select, type) { $scope.select = function(selected, select, type) {
select.search = undefined; select.search = undefined;
@ -405,6 +433,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
selected.value = $scope.old[type][selected.name]; selected.value = $scope.old[type][selected.name];
} }
$scope[type].push(selected); $scope[type].push(selected);
console.log(selected);
console.log($scope[type]);
$scope.add[type] = false; $scope.add[type] = false;
$scope.changed = true; $scope.changed = true;
}; };
@ -458,19 +488,46 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
}; };
$scope.getClientsList = function() { $scope.getClientsList = function() {
api = '{{ url_for("api.clients_list", server=server) }}'; api = '{{ url_for("api.clients_list", server=server) }}';
$.ajax({ $http.get(
url: api, api,
type: 'GET' {
}).done(function(data) { headers: { 'X-From-UI': true },
$scope.clients = data.result; }
}); ).then(
function(response) {
var data = response.data;
$scope.clients = data.result;
}
);
};
$scope.getTemplatesList = function() {
api = '{{ url_for("api.templates_list", server=server) }}';
$http.get(
api,
{
headers: { 'X-From-UI': true },
}
).then(
function(response) {
var data = response.data;
$scope.raw.templates = data.result;
$scope.all.templates = {};
_(data.result).forEach(function(r) {
$scope.all.templates[r.name] = r.value;
});
}
);
}; };
$scope.deleteClient = function() { $scope.deleteClient = function() {
api = '{{ url_for("api.client_settings", client=client, server=server) }}'; api = '{{ url_for("api.client_settings", client=client, server=server) }}';
$.ajax({ $.ajax({
url: api, url: api,
type: 'DELETE', type: 'DELETE',
{% if template -%}
data: { template: true }
{% else -%}
data: { delcert: $('#delcert').is(':checked'), revoke: $('#revoke').is(':checked'), keepconf: $('#keepconf').is(':checked') } data: { delcert: $('#delcert').is(':checked'), revoke: $('#revoke').is(':checked'), keepconf: $('#keepconf').is(':checked') }
{% endif -%}
}) })
.fail(myFail) .fail(myFail)
.done(function(data) { .done(function(data) {
@ -503,6 +560,28 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
} }
}); });
}; };
$scope.createTemplate = function(e) {
/* we disable the 'real' form submission */
e.preventDefault();
var form = $(e.target);
$.ajax({
url: form.attr('action'),
type: 'PUT',
data: form.serialize()
})
.fail(myFail)
.done(function(data) {
/* The server answered correctly but some errors may have occurred server
* side so we display them */
if (data.notif) {
notif(data.notif[0][0], data.notif[0][1]);
if (data.notif[0][0] == NOTIF_SUCCESS) {
$scope.getTemplatesList();
notif(data.notif[1][0], data.notif[1][1], 20000);
}
}
});
};
/* These callbacks expand/reduce the input for a better readability */ /* These callbacks expand/reduce the input for a better readability */
$scope.focusIn = function(ev) { $scope.focusIn = function(ev) {
el = $( ev.target ).parent(); el = $( ev.target ).parent();
@ -522,8 +601,14 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
el.next('div').next('div').next('div').show(); el.next('div').next('div').next('div').show();
el.removeClass('col-lg-9').addClass('col-lg-2'); el.removeClass('col-lg-9').addClass('col-lg-2');
}; };
$scope.loadConfig();
$scope.getClientsList();
$scope.getTemplatesList();
}]); }]);
{{ macros.page_length('#table-list-clients') }}
{{ macros.page_length('#table-list-templates') }}
$(document).ready(function () { $(document).ready(function () {
$('#config-nav a').click(function (e) { $('#config-nav a').click(function (e) {
e.preventDefault(); e.preventDefault();

View file

@ -103,6 +103,7 @@
<script src="{{ url_for('bower.static', filename='angular-sanitize/angular-sanitize.min.js') }}"></script> <script src="{{ url_for('bower.static', filename='angular-sanitize/angular-sanitize.min.js') }}"></script>
<script src="{{ url_for('bower.static', filename='angular-resource/angular-resource.min.js') }}"></script> <script src="{{ url_for('bower.static', filename='angular-resource/angular-resource.min.js') }}"></script>
<script src="{{ url_for('bower.static', filename='angular-animate/angular-animate.min.js') }}"></script> <script src="{{ url_for('bower.static', filename='angular-animate/angular-animate.min.js') }}"></script>
<script src="{{ url_for('bower.static', filename='angular-datatables-0.6.2/dist/angular-datatables.min.js') }}"></script>
{% endif -%} {% endif -%}
{% if tree or settings -%} {% if tree or settings -%}
<!-- Fancytree Javascript <!-- Fancytree Javascript

View file

@ -52,6 +52,7 @@
<li class="active"><a href="#config" data-toggle="tab" aria-expanded="true">{{ _('Config') }}</a></li> <li class="active"><a href="#config" data-toggle="tab" aria-expanded="true">{{ _('Config') }}</a></li>
<li><a href="#hierarchy" data-toggle="tab" aria-expanded="false">{{ _('Hierarchy') }}</a></li> <li><a href="#hierarchy" data-toggle="tab" aria-expanded="false">{{ _('Hierarchy') }}</a></li>
<li><a href="#clients" data-toggle="tab" aria-expanded="false">{{ _('Clients') }}</a></li> <li><a href="#clients" data-toggle="tab" aria-expanded="false">{{ _('Clients') }}</a></li>
<li><a href="#list-templates" data-toggle="tab" aria-expanded="false">{{ _('Templates') }}</a></li>
</ul> </ul>
<div id="config-tab-content" class="tab-content"> <div id="config-tab-content" class="tab-content">
<div class="tab-pane fade active in" id="config"> <div class="tab-pane fade active in" id="config">
@ -65,7 +66,11 @@
<div id="settings-panel" class="form-container" style="display:none;" ng-cloak> <div id="settings-panel" class="form-container" style="display:none;" ng-cloak>
{% if client -%} {% if client -%}
{% if template -%}
<form class="form-horizontal" action="{{ url_for('api.client_settings', client=client, conf=conf, template=template, server=server) }}" method="POST" ng-submit="submit($event)" name="setSettings" onbeforeunload>
{% else -%}
<form class="form-horizontal" action="{{ url_for('api.client_settings', client=client, conf=conf, server=server) }}" method="POST" ng-submit="submit($event)" name="setSettings" onbeforeunload> <form class="form-horizontal" action="{{ url_for('api.client_settings', client=client, conf=conf, server=server) }}" method="POST" ng-submit="submit($event)" name="setSettings" onbeforeunload>
{% endif -%}
{% else -%} {% else -%}
<form class="form-horizontal" action="{{ url_for('api.server_settings', conf=conf, server=server) }}" method="POST" ng-submit="submit($event)" name="setSettings" onbeforeunload> <form class="form-horizontal" action="{{ url_for('api.server_settings', conf=conf, server=server) }}" method="POST" ng-submit="submit($event)" name="setSettings" onbeforeunload>
{% endif -%} {% endif -%}
@ -263,13 +268,59 @@
</div> </div>
<div class="form-group"> <div class="form-group">
{% endraw %} {% endraw %}
<label for="btn-add-include" class="col-lg-3 control-label">{{ _('source config') }}</label> <label for="btn-add-include" class="col-lg-3 control-label">{{ _('source now config') }}</label>
<div class="col-lg-9"> <div class="col-lg-9">
<button type="button" class="btn btn-success btn-primary" ng-click="clickAddIncludes()"><span class="glyphicon glyphicon-plus"></span></button> <button type="button" class="btn btn-success btn-primary" ng-click="clickAddIncludes()"><span class="glyphicon glyphicon-plus"></span></button>
</div> </div>
</div> </div>
</div> </div>
</fieldset> </fieldset>
{% if client -%}
<fieldset>
<legend id="templates">{{ _('Templates') }}</legend>
{% raw %}
<div class="well">
<div class="form-group" ng-repeat="template in templates track by $index" ng-class="{'has-error': inc_invalid[$index] }">
<label for="template-{{ $index }}" class="col-lg-3 control-label">
{% endraw %}
{{ _('template') }}
{% raw %}
</label>
<div class="col-lg-2">
<input class="form-control" type="text" id="template-{{ $index }}" ng-model="template.name" ng-focus="focusIn($event)" ng-blur="focusOut($event)" placeholder="{{ ::placeholders['.'] }}" readonly>
<input type="hidden" name="templates" id="template-hidden-{{ $index }}" value="{{ template.value }}">
</div>
<div class="col-lg-2 btn-toolbar">
<button type="button" class="btn btn-danger" ng-click="remove('templates', $index)"><span class="glyphicon glyphicon-minus"></span></button>
</div>
<div class="col-lg-5" ng-if="$first" ng-bind-html="::server_doc['.']"></div>
</div>
<div class="form-group" ng-hide="!add.templates">
<div class="col-lg-3">
<ui-select ng-model="new.templates" ng-disabled="!add.templates" style="width: 100%;" on-select="select($item, $select, 'templates')">
{% endraw %}
<ui-select-match placeholder="{{ _('Select a template') }}">
{% raw %}
{{ $select.selected.name }}
{% endraw %}
</ui-select-match>
<ui-select-choices repeat="value.name as value in avail.templates | filter: $select.search">
<div ng-bind-html="value.name | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
</div>
<div class="col-lg-9">
<button type="button" class="btn btn-danger" ng-click="undoAdd('templates')"><span class="glyphicon glyphicon-remove"></span></button>
</div>
</div>
<div class="form-group" ng-hide="templates.length >= raw.templates.length || add.templates">
<div class="col-lg-3 col-lg-offset-3">
<button type="button" class="btn btn-success btn-primary" ng-click="clickAdd('templates')"><span class="glyphicon glyphicon-plus"></span></button>
</div>
</div>
</div>
</fieldset>
{% endif -%}
<div class="btn-toolbar"> <div class="btn-toolbar">
<div class="col-lg-3 col-lg-offset-3"> <div class="col-lg-3 col-lg-offset-3">
<button type="submit" class="btn btn-primary" ng-disabled="!setSettings.$valid">{{ _('Save') }}</button> <button type="submit" class="btn btn-primary" ng-disabled="!setSettings.$valid">{{ _('Save') }}</button>
@ -278,11 +329,14 @@
{% if client and not conf -%} {% if client and not conf -%}
<div class="btn-group dropupi text-right"> <div class="btn-group dropupi text-right">
<button type="button" class="btn btn-danger" ng-click="deleteClient()">{{ _("Remove '%(client)s'", client=client) }}</button> <button type="button" class="btn btn-danger" ng-click="deleteClient()">{{ _("Remove '%(client)s'", client=client) }}</button>
{% if not template -%}
<button class="btn btn-danger dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button> <button class="btn btn-danger dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
<ul class="dropdown-menu browse"> <ul class="dropdown-menu browse">
<li><label for="keepconf">{{ _('Do not remove the configuration:') }}&nbsp;</label><input type="checkbox" id="keepconf" name="keepconf"></li> <li><label for="keepconf">{{ _('Do not remove the configuration:') }}&nbsp;</label><input type="checkbox" id="keepconf" name="keepconf"></li>
<li><label for="delcert">{{ _('Remove associated certificate:') }}&nbsp;</label><input type="checkbox" id="delcert" name="delcert"></li> <li><label for="delcert">{{ _('Remove associated certificate:') }}&nbsp;</label><input type="checkbox" id="delcert" name="delcert"></li>
<li><label for="revoke">{{ _('Revoke associated certificate:') }}&nbsp;</label><input type="checkbox" id="revoke" name="revoke" ng-disabled="!revokeEnabled"></li> <li><label for="revoke">{{ _('Revoke associated certificate:') }}&nbsp;</label><input type="checkbox" id="revoke" name="revoke" ng-disabled="!revokeEnabled"></li>
</ul>
{% endif -%}
</div> </div>
{% endif -%} {% endif -%}
</div> </div>
@ -305,7 +359,7 @@
<div class="tab-pane fade" id="clients"> <div class="tab-pane fade" id="clients">
<div style="padding-top: 80px; margin-top: -45px;"></div> <div style="padding-top: 80px; margin-top: -45px;"></div>
<div id="table-clients" class="table-responsive row"> <div id="table-clients" class="table-responsive row">
<table class="table table-striped table-hover nowrap" width="100%" datatable="ng" dt-options="dtOptions" dt-column-defs="dtColumnDefs"> <table class="table table-striped table-hover nowrap" width="100%" datatable="ng" dt-options="dtOptions" dt-column-defs="dtColumnDefs" id="table-list-clients">
<thead> <thead>
<tr> <tr>
<th>{{ _('Name') }}</th><th>{{ _('Path') }}</th><th></th> <th>{{ _('Name') }}</th><th>{{ _('Path') }}</th><th></th>
@ -317,11 +371,58 @@
<td>{{ client.name }}</td> <td>{{ client.name }}</td>
<td>{{ client.value }}</td> <td>{{ client.value }}</td>
{% endraw -%} {% endraw -%}
<td><a href="{{ url_for("view.cli_settings", server=server) }}?client={{ '{{' }} client.name {{ '}}' }}" class="btn btn-info btn-xs no-link pull-right"><span class="glyphicon glyphicon-pencil" aria-hidden="true">&nbsp;Edit</a></td> <td><a href="{{ url_for("view.cli_settings", server=server) }}?client={{ '{{' }} client.name {{ '}}' }}" class="btn btn-info btn-xs no-link pull-right"><span class="glyphicon glyphicon-pencil" aria-hidden="true"> {{ _('edit') }}</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="row well">
<form action="{{ url_for('api.new_client', server=server) }}" method="POST" ng-submit="createClient($event)">
<fieldset>
<Legend>{{ _('Create new client') }}</Legend>
<div class="input-group col-lg-3">
<input class="form-control" type="text" name="newclient" id="newclient" placeholder="{{ _('Create new client') }}">
<span class="input-group-btn">
<button class="btn btn-success" type="submit"><span class="glyphicon glyphicon-plus"></span></button>
</span>
</div>
</fieldset>
</form>
</div>
</div>
<div class="tab-pane fade" id="list-templates">
<div style="padding-top: 80px; margin-top: -45px;"></div>
<div id="table-templates" class="table-responsive row">
<table class="table table-striped table-hover nowrap" width="100%" datatable="ng" dt-options="dtOptions" dt-column-defs="dtColumnDefs" id="table-list-templates">
<thead>
<tr>
<th>{{ _('Name') }}</th><th>{{ _('Path') }}</th><th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="template in raw.templates">
{% raw -%}
<td>{{ template.name }}</td>
<td>{{ template.value }}</td>
{% endraw -%}
<td><a href="{{ url_for("view.cli_settings", server=server) }}?client={{ '{{' }} template.name {{ '}}' }}&template=true" class="btn btn-info btn-xs no-link pull-right"><span class="glyphicon glyphicon-pencil" aria-hidden="true"> {{ _('edit') }}</a></td>
</tr>
</tbody>
</table>
</div>
<div class="row well">
<form action="{{ url_for('api.new_template', server=server) }}" method="POST" ng-submit="createTemplate($event)">
<fieldset>
<Legend>{{ _('Create new template') }}</Legend>
<div class="input-group col-lg-3">
<input class="form-control" type="text" name="newtemplate" id="newtemplate" placeholder="{{ _('Create new template') }}">
<span class="input-group-btn">
<button class="btn btn-success" type="submit"><span class="glyphicon glyphicon-plus"></span></button>
</span>
</div>
</fieldset>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -7,18 +7,8 @@
<li data-target="#integer"><a class="scroll" href="#integer">{{ _('Integers') }}</a></li> <li data-target="#integer"><a class="scroll" href="#integer">{{ _('Integers') }}</a></li>
<li data-target="#multi"><a class="scroll" href="#multi">{{ _('Multi') }}</a></li> <li data-target="#multi"><a class="scroll" href="#multi">{{ _('Multi') }}</a></li>
<li data-target="#includes_source"><a class="scroll" href="#includes_source">{{ _('Source files') }}</a></li> <li data-target="#includes_source"><a class="scroll" href="#includes_source">{{ _('Source files') }}</a></li>
</ul> {% if client and not template -%}
<h4>{{ _('Client to configure') }}</h4> <li data-target="#templates"><a class="scroll" href="#templates">{{ _('Templates') }}</a></li>
<ul class="nav nav-sidebar"> {% endif -%}
<li>
<form action="{{ url_for('api.new_client', server=server) }}" method="POST" ng-submit="createClient($event)">
<div class="input-group">
<input class="form-control" type="text" name="newclient" id="newclient" placeholder="{{ _('Create new client') }}">
<span class="input-group-btn">
<button class="btn btn-success" type="submit"><span class="glyphicon glyphicon-plus"></span></button>
</span>
</div>
</form>
</li>
</ul> </ul>
</div> </div>

View file

@ -30,6 +30,8 @@ v0.6.0
- **New** - WebSocket support for better/smarter notifications. - **New** - WebSocket support for better/smarter notifications.
- **New** - Client configuration templates.
v0.5.0 v0.5.0
------ ------

View file

@ -64,6 +64,7 @@ VENDOR_TO_KEEP = [
'burpui/static/vendor/angular-strap/dist/angular-strap.min.js', 'burpui/static/vendor/angular-strap/dist/angular-strap.min.js',
'burpui/static/vendor/angular-strap/dist/angular-strap.tpl.min.js', 'burpui/static/vendor/angular-strap/dist/angular-strap.tpl.min.js',
'burpui/static/vendor/angular-onbeforeunload/build/angular-onbeforeunload.js', 'burpui/static/vendor/angular-onbeforeunload/build/angular-onbeforeunload.js',
'burpui/static/vendor/angular-datatables-0.6.2/dist/angular-datatables.min.js',
'burpui/static/vendor/moment/min/moment.min.js', 'burpui/static/vendor/moment/min/moment.min.js',
'burpui/static/vendor/moment/locale/fr.js', 'burpui/static/vendor/moment/locale/fr.js',
'burpui/static/vendor/moment/locale/es.js', 'burpui/static/vendor/moment/locale/es.js',

View file

@ -140,7 +140,6 @@ class BurpuiAPITestCase(TestCase):
u'multi': [], u'multi': [],
u'includes': [], u'includes': [],
u'includes_ext': [], u'includes_ext': [],
u'clients': [],
u'hierarchy': [{u'children': [], u'title': u'null', u'dir': u'/dev', u'full': u'/dev/null', u'name': u'null', u'parent': None}], u'hierarchy': [{u'children': [], u'title': u'null', u'dir': u'/dev', u'full': u'/dev/null', u'name': u'null', u'parent': None}],
} }
), ),
@ -167,8 +166,8 @@ class BurpuiAPITestCase(TestCase):
u'multi': [], u'multi': [],
u'includes': [], u'includes': [],
u'includes_ext': [], u'includes_ext': [],
u'clients': [],
u'hierarchy': [], u'hierarchy': [],
u'templates': [],
} }
), ),
(u'boolean', self.bui.client.get_parser_attr('boolean_cli')), (u'boolean', self.bui.client.get_parser_attr('boolean_cli')),