mirror of
https://github.com/ziirish/burp-ui.git
synced 2026-05-21 06:45:24 -06:00
add: client configuration templates (fix #155)
This commit is contained in:
parent
fad4cf2898
commit
5279f0b019
16 changed files with 642 additions and 141 deletions
|
|
@ -9,6 +9,7 @@ Current
|
|||
- **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: `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: `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>`_
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from ..utils import NOTIF_INFO
|
|||
|
||||
from six import iteritems
|
||||
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
|
||||
|
||||
bui = current_app # type: BUIServer
|
||||
|
|
@ -236,21 +236,40 @@ class ServerSettings(Resource):
|
|||
res = bui.client.read_conf_srv(conf, server)
|
||||
refresh()
|
||||
# Translate the doc and placeholder API side
|
||||
doc = bui.client.get_parser_attr('doc', server).copy()
|
||||
placeholders = bui.client.get_parser_attr('placeholders', server).copy()
|
||||
for key, val in iteritems(doc):
|
||||
doc[key] = _(val)
|
||||
for key, val in iteritems(placeholders):
|
||||
placeholders[key] = _(val)
|
||||
cache_keys = {
|
||||
'doc': '_doc_parser_{}-{}'.format(server, g.locale),
|
||||
'placeholders': '_placeholders_parser_{}-{}'.format(server, g.locale),
|
||||
'boolean_srv': '_boolean_srv_parser_{}'.format(server),
|
||||
'string_srv': '_string_srv_parser_{}'.format(server),
|
||||
'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,
|
||||
boolean=bui.client.get_parser_attr('boolean_srv', server),
|
||||
string=bui.client.get_parser_attr('string_srv', server),
|
||||
integer=bui.client.get_parser_attr('integer_srv', server),
|
||||
multi=bui.client.get_parser_attr('multi_srv', server),
|
||||
server_doc=doc,
|
||||
suggest=bui.client.get_parser_attr('values', server),
|
||||
placeholders=placeholders,
|
||||
defaults=bui.client.get_parser_attr('defaults', server))
|
||||
boolean=cache_results['boolean_srv'],
|
||||
string=cache_results['string_srv'],
|
||||
integer=cache_results['integer_srv'],
|
||||
multi=cache_results['multi_srv'],
|
||||
server_doc=cache_results['doc'],
|
||||
suggest=cache_results['values'],
|
||||
placeholders=cache_results['placeholders'],
|
||||
defaults=cache_results['defaults'])
|
||||
|
||||
|
||||
@ns.route('/clients',
|
||||
|
|
@ -273,10 +292,84 @@ class ClientsList(Resource):
|
|||
)
|
||||
def get(self, server=None):
|
||||
"""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)
|
||||
|
||||
|
||||
@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',
|
||||
'/<server>/config',
|
||||
endpoint='new_client',
|
||||
|
|
@ -306,7 +399,8 @@ class NewClientSettings(Resource):
|
|||
newclient = self.parser.parse_args()['newclient']
|
||||
if not newclient:
|
||||
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:
|
||||
if cl['name'] == 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('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('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.acl_admin_required(message=_('Sorry, you don\'t have rights to access the setting panel'))
|
||||
@ns.expect(parser_post)
|
||||
@ns.doc(
|
||||
responses={
|
||||
200: 'Success',
|
||||
|
|
@ -357,10 +457,13 @@ class ClientSettings(Resource):
|
|||
)
|
||||
def post(self, server=None, client=None, conf=None):
|
||||
"""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}
|
||||
|
||||
@api.acl_admin_required(message=_('Sorry, you don\'t have rights to access the setting panel'))
|
||||
@ns.expect(parser_get)
|
||||
@ns.doc(
|
||||
responses={
|
||||
200: 'Success',
|
||||
|
|
@ -374,25 +477,47 @@ class ClientSettings(Resource):
|
|||
conf = unquote(conf)
|
||||
except:
|
||||
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()
|
||||
# Translate the doc and placeholder API side
|
||||
doc = bui.client.get_parser_attr('doc', server).copy()
|
||||
placeholders = bui.client.get_parser_attr('placeholders', server).copy()
|
||||
for key, val in iteritems(doc):
|
||||
doc[key] = _(val)
|
||||
for key, val in iteritems(placeholders):
|
||||
placeholders[key] = _(val)
|
||||
cache_keys = {
|
||||
'doc': '_doc_parser_{}-{}'.format(server, g.locale),
|
||||
'placeholders': '_placeholders_parser_{}-{}'.format(server, g.locale),
|
||||
'boolean_cli': '_boolean_cli_parser_{}'.format(server),
|
||||
'string_cli': '_string_cli_parser_{}'.format(server),
|
||||
'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(
|
||||
results=res,
|
||||
boolean=bui.client.get_parser_attr('boolean_cli', server),
|
||||
string=bui.client.get_parser_attr('string_cli', server),
|
||||
integer=bui.client.get_parser_attr('integer_cli', server),
|
||||
multi=bui.client.get_parser_attr('multi_cli', server),
|
||||
server_doc=doc,
|
||||
suggest=bui.client.get_parser_attr('values', server),
|
||||
placeholders=placeholders,
|
||||
defaults=bui.client.get_parser_attr('defaults', server)
|
||||
boolean=cache_results['boolean_cli'],
|
||||
string=cache_results['string_cli'],
|
||||
integer=cache_results['integer_cli'],
|
||||
multi=cache_results['multi_cli'],
|
||||
server_doc=cache_results['doc'],
|
||||
suggest=cache_results['values'],
|
||||
placeholders=cache_results['placeholders'],
|
||||
defaults=cache_results['defaults']
|
||||
)
|
||||
|
||||
@api.disabled_on_demo()
|
||||
|
|
@ -411,6 +536,7 @@ class ClientSettings(Resource):
|
|||
delcert = args.get('delcert', False)
|
||||
revoke = args.get('revoke', False)
|
||||
keepconf = args.get('keepconf', False)
|
||||
template = args.get('template', False)
|
||||
|
||||
if not keepconf:
|
||||
# clear the cache when we remove a client
|
||||
|
|
@ -418,7 +544,8 @@ class ClientSettings(Resource):
|
|||
if bui.config['WITH_CELERY']:
|
||||
from ..tasks import 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',
|
||||
|
|
@ -459,7 +586,8 @@ class PathExpander(Resource):
|
|||
path = unquote(path)
|
||||
if 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:
|
||||
self.abort(403, 'Path not found')
|
||||
return {'result': paths}
|
||||
|
|
|
|||
|
|
@ -1000,7 +1000,7 @@ class Burp(BUIbackend):
|
|||
return []
|
||||
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`"""
|
||||
if not self.parser:
|
||||
return []
|
||||
|
|
@ -1008,7 +1008,7 @@ class Burp(BUIbackend):
|
|||
conf = unquote(conf)
|
||||
except:
|
||||
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):
|
||||
"""See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv`"""
|
||||
|
|
|
|||
|
|
@ -806,7 +806,7 @@ class BUIbackend(with_metaclass(ABCMeta, object)):
|
|||
raise NotImplementedError("Sorry, the current Backend does not implement this method!") # pragma: no cover
|
||||
|
||||
@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`
|
||||
function works the same way as the
|
||||
:func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv` function
|
||||
|
|
|
|||
|
|
@ -511,7 +511,7 @@ class NClient(BUIbackend):
|
|||
"""
|
||||
|
||||
@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`"""
|
||||
# serialize data as it is a nested dict
|
||||
import hmac
|
||||
|
|
@ -527,7 +527,7 @@ class NClient(BUIbackend):
|
|||
data = ImmutableMultiDict(data.to_dict(False))
|
||||
key = '{}{}'.format(self.password, 'store_conf_cli')
|
||||
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)
|
||||
digest = hmac.new(key, bytes_pickles, hashlib.sha1).hexdigest()
|
||||
data = {'func': 'store_conf_cli', 'args': pickles, 'pickled': True, 'digest': digest}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,12 @@ class Parser(Doc):
|
|||
self._server_conf = {}
|
||||
self._client_conf = {}
|
||||
self._clients_conf = {}
|
||||
self._templates_conf = {}
|
||||
self.clientconfdir = None
|
||||
self.clientconfdir_mtime = None
|
||||
self.templates = []
|
||||
self.templates_dir = '.buitemplates'
|
||||
self.templates_path = None
|
||||
self.filescache = {}
|
||||
self._configs = {}
|
||||
self.root = None
|
||||
|
|
@ -89,6 +93,7 @@ class Parser(Doc):
|
|||
self._server_conf.clear()
|
||||
self._client_conf.clear()
|
||||
self._clients_conf.clear()
|
||||
self._list_templates(True)
|
||||
self._list_clients(True)
|
||||
|
||||
def _load_conf_srv(self):
|
||||
|
|
@ -96,6 +101,7 @@ class Parser(Doc):
|
|||
self._server_conf = Config(self.conf, self, 'srv')
|
||||
self._server_conf.parse()
|
||||
self.clientconfdir = self._server_conf.get('clientconfdir')
|
||||
self.templates_path = os.path.join(self.clientconfdir, self.templates_dir)
|
||||
|
||||
def _load_conf_cli(self):
|
||||
"""Load the client configuration file"""
|
||||
|
|
@ -120,11 +126,26 @@ class Parser(Doc):
|
|||
conf.parse()
|
||||
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):
|
||||
"""Load all configurations"""
|
||||
self._load_conf_srv()
|
||||
self._load_conf_cli()
|
||||
self._load_conf_clients()
|
||||
self._load_conf_templates()
|
||||
|
||||
def _new_client_conf(self, name, path):
|
||||
"""Create new client conf"""
|
||||
|
|
@ -153,6 +174,17 @@ class Parser(Doc):
|
|||
self._clients_conf[name].parse()
|
||||
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'):
|
||||
"""Return conf by it's path"""
|
||||
if path in self._configs:
|
||||
|
|
@ -200,6 +232,27 @@ class Parser(Doc):
|
|||
self.clientconfdir_mtime = os.path.getmtime(self.clientconfdir)
|
||||
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):
|
||||
"""Returns the path of the 'server *fil*' file"""
|
||||
if not name:
|
||||
|
|
@ -256,7 +309,7 @@ class Parser(Doc):
|
|||
return False
|
||||
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`"""
|
||||
res = []
|
||||
revoked = False
|
||||
|
|
@ -265,12 +318,15 @@ class Parser(Doc):
|
|||
return [[NOTIF_ERROR, "No client provided"]]
|
||||
try:
|
||||
if not keepconf:
|
||||
if template:
|
||||
path = os.path.join(self.templates_path, client)
|
||||
else:
|
||||
path = os.path.join(self.clientconfdir, client)
|
||||
os.unlink(path)
|
||||
res.append([NOTIF_OK, "'{}' successfully removed".format(client)])
|
||||
removed = True
|
||||
|
||||
if client in self._clients_conf:
|
||||
if client in self._clients_conf and not template:
|
||||
del self._clients_conf[client]
|
||||
|
||||
self._refresh_cache()
|
||||
|
|
@ -302,7 +358,7 @@ class Parser(Doc):
|
|||
|
||||
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`
|
||||
"""
|
||||
|
|
@ -313,7 +369,7 @@ class Parser(Doc):
|
|||
u'multi': [],
|
||||
u'includes': [],
|
||||
u'includes_ext': [],
|
||||
u'clients': self._list_clients(),
|
||||
u'templates': [],
|
||||
u'hierarchy': [],
|
||||
}
|
||||
if not client and not conf:
|
||||
|
|
@ -323,6 +379,10 @@ class Parser(Doc):
|
|||
if not mconf:
|
||||
if not self.clientconfdir:
|
||||
return res
|
||||
if template:
|
||||
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:
|
||||
|
|
@ -337,6 +397,7 @@ class Parser(Doc):
|
|||
res2[u'boolean'] = parsed.boolean
|
||||
res2[u'integer'] = parsed.integer
|
||||
res2[u'multi'] = parsed.multi
|
||||
res2[u'templates'] = parsed.template
|
||||
res2[u'includes'] = [
|
||||
x
|
||||
for x in parsed.flatten('include', False).keys()
|
||||
|
|
@ -363,7 +424,6 @@ class Parser(Doc):
|
|||
u'multi': [],
|
||||
u'includes': [],
|
||||
u'includes_ext': [],
|
||||
u'clients': self._list_clients(),
|
||||
u'hierarchy': [],
|
||||
}
|
||||
if not conf:
|
||||
|
|
@ -378,10 +438,6 @@ class Parser(Doc):
|
|||
return self.filescache[mconf]['dict']
|
||||
|
||||
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[u'common'] = parsed.string
|
||||
|
|
@ -405,34 +461,43 @@ class Parser(Doc):
|
|||
def list_clients(self):
|
||||
"""See :func:`burpui.misc.parser.interface.BUIparser.list_clients`"""
|
||||
self.read_server_conf()
|
||||
if not self.clientconfdir:
|
||||
return []
|
||||
|
||||
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`
|
||||
"""
|
||||
if conf and not os.path.isabs(conf):
|
||||
conf = os.path.join(self.clientconfdir, conf)
|
||||
if not conf and not client:
|
||||
if template:
|
||||
return [[NOTIF_ERROR, 'Sorry, no template defined']]
|
||||
return [[NOTIF_ERROR, 'Sorry, no client defined']]
|
||||
elif client and not conf:
|
||||
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')
|
||||
self._refresh_cache() # refresh client list
|
||||
return ret
|
||||
|
||||
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`"""
|
||||
mconf = None
|
||||
if not conf:
|
||||
mconf = self.conf
|
||||
else:
|
||||
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)
|
||||
if not mconf:
|
||||
return [[NOTIF_WARN, 'Sorry, no configuration file defined']]
|
||||
|
|
@ -447,7 +512,9 @@ class Parser(Doc):
|
|||
]
|
||||
|
||||
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)
|
||||
else:
|
||||
conffile = self.server_conf.get_file(mconf)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
.. moduleauthor:: Ziirish <hi+burpui@ziirish.me>
|
||||
|
||||
"""
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
from six import with_metaclass
|
||||
|
||||
import logging
|
||||
|
|
@ -134,7 +134,7 @@ class BUIparser(with_metaclass(ABCMeta, object)):
|
|||
) # pragma: no cover
|
||||
|
||||
@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
|
||||
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
|
||||
: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(
|
||||
"Sorry, the current Parser does not implement this method!"
|
||||
|
|
@ -151,7 +157,7 @@ class BUIparser(with_metaclass(ABCMeta, object)):
|
|||
|
||||
@abstractmethod
|
||||
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
|
||||
store the configuration from the web-ui into the actual configuration
|
||||
files.
|
||||
|
|
@ -173,6 +179,9 @@ class BUIparser(with_metaclass(ABCMeta, object)):
|
|||
:param insecure: Used for the CLI
|
||||
:type insecure: bool
|
||||
|
||||
:param template: Is it a template
|
||||
:type template: bool
|
||||
|
||||
:returns: A list of notifications to return to the UI (success or
|
||||
failure)
|
||||
|
||||
|
|
@ -216,6 +225,17 @@ class BUIparser(with_metaclass(ABCMeta, object)):
|
|||
"Sorry, the current Parser does not implement this method!"
|
||||
) # 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
|
||||
def is_client_revoked(self, client=None):
|
||||
""":func:`burpui.misc.parser.interface.BUIparser.is_client_revoked` is
|
||||
|
|
@ -231,7 +251,7 @@ class BUIparser(with_metaclass(ABCMeta, object)):
|
|||
) # pragma: no cover
|
||||
|
||||
@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
|
||||
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
|
||||
: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
|
||||
failure)
|
||||
"""
|
||||
|
|
@ -255,7 +278,7 @@ class BUIparser(with_metaclass(ABCMeta, object)):
|
|||
) # pragma: no cover
|
||||
|
||||
@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
|
||||
called by :func:`burpui.misc.backend.interface.BUIbackend.read_conf_cli`
|
||||
in order to parse the burp-clients configuration files.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ from ...datastructures import MultiDict
|
|||
|
||||
|
||||
RESET_IDENTIFIER = '_reset_bui_CUSTOM'
|
||||
BEGIN_TEMPLATES = 'BURP-UI TEMPLATES'
|
||||
END_TEMPLATES = 'END TEMPLATES'
|
||||
|
||||
|
||||
class Option(object):
|
||||
|
|
@ -272,7 +274,7 @@ class OptionInc(Option):
|
|||
|
||||
def _path_absolute(self, path):
|
||||
absolute = path
|
||||
if not path.startswith('/'):
|
||||
if not os.path.isabs(path):
|
||||
if self.root:
|
||||
absolute = os.path.join(self.root, path)
|
||||
elif self.mode == 'srv':
|
||||
|
|
@ -321,6 +323,60 @@ class OptionInc(Option):
|
|||
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):
|
||||
"""Object representing a configuration file
|
||||
|
||||
|
|
@ -345,6 +401,8 @@ class File(dict):
|
|||
self._dirty = False
|
||||
# _changed is used to know if the file changed since last read
|
||||
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
|
||||
self._raw = []
|
||||
self._raw_data = MultiDict()
|
||||
|
|
@ -362,6 +420,7 @@ class File(dict):
|
|||
'include': OrderedDict(),
|
||||
'multi': OrderedDict(),
|
||||
'string': OrderedDict(),
|
||||
'template': OrderedDict(),
|
||||
}
|
||||
if self.name:
|
||||
self.parse()
|
||||
|
|
@ -372,6 +431,10 @@ class File(dict):
|
|||
if val.dirty:
|
||||
self._changed = True
|
||||
return self._changed
|
||||
for key, val in iteritems(self.types['template']):
|
||||
if val.dirty:
|
||||
self._changed = True
|
||||
return self._changed
|
||||
try:
|
||||
if self.name:
|
||||
mtime = os.path.getmtime(self.name)
|
||||
|
|
@ -463,6 +526,16 @@ class File(dict):
|
|||
def include(self):
|
||||
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
|
||||
def multi(self):
|
||||
return self.flatten('multi')
|
||||
|
|
@ -506,7 +579,7 @@ class File(dict):
|
|||
return opt
|
||||
if typ == 'include':
|
||||
key = value
|
||||
opt = OptionInc(
|
||||
return OptionInc(
|
||||
self.parser,
|
||||
key,
|
||||
value,
|
||||
|
|
@ -548,6 +621,9 @@ class File(dict):
|
|||
opt.append(value)
|
||||
elif key == u'.':
|
||||
key = value
|
||||
if self._parsing_templates:
|
||||
opt = OptionTpl(self.parser, key, value)
|
||||
else:
|
||||
opt = OptionInc(
|
||||
self.parser,
|
||||
key,
|
||||
|
|
@ -698,6 +774,10 @@ class File(dict):
|
|||
self.clear()
|
||||
for line in self.raw:
|
||||
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
|
||||
res = re.search(r'\s*([^=\s]+)\s*(:)?=?\s*(.*)$', line)
|
||||
if res:
|
||||
|
|
@ -821,7 +901,21 @@ class File(dict):
|
|||
with codecs.open(dest, 'w', 'utf-8', errors='ignore') as fil:
|
||||
# f.write('# Auto-generated configuration using Burp-UI\n')
|
||||
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):
|
||||
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)
|
||||
if (self._line_removed(line, data_keys) and
|
||||
not self._line_is_comment(line) and
|
||||
|
|
@ -917,7 +1011,7 @@ class File(dict):
|
|||
if key.endswith(RESET_IDENTIFIER):
|
||||
continue
|
||||
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(
|
||||
fil,
|
||||
key,
|
||||
|
|
@ -1016,6 +1110,7 @@ class Config(File):
|
|||
self.default = path
|
||||
self.name = path
|
||||
self._includes = []
|
||||
self._templates = []
|
||||
self._dirty = True
|
||||
if path:
|
||||
self.files[path] = File(parser, path, mode=mode)
|
||||
|
|
@ -1039,6 +1134,11 @@ class Config(File):
|
|||
path = os.path.join(os.path.dirname(root), path)
|
||||
self.add_file(path, root)
|
||||
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
|
||||
if orig != self.files:
|
||||
|
|
@ -1049,13 +1149,14 @@ class Config(File):
|
|||
return
|
||||
|
||||
del self._includes[:]
|
||||
del self._templates[:]
|
||||
self._parse()
|
||||
|
||||
removed = []
|
||||
orig = self.files
|
||||
for path, conf in iteritems(orig):
|
||||
if conf.parent and (conf.name not in self._includes or
|
||||
conf.name in removed):
|
||||
if conf.parent and ((conf.name not in self._includes and
|
||||
conf.name not in self._templates) or conf.name in removed):
|
||||
removed.append(path)
|
||||
self.del_file(path)
|
||||
|
||||
|
|
@ -1110,7 +1211,7 @@ class Config(File):
|
|||
if conf and conf in self.files:
|
||||
return self.files[conf].store(dest, insecure)
|
||||
for name, conf in iteritems(self.files):
|
||||
ret += conf.store(dest, insecure)
|
||||
ret += conf.store(insecure=insecure)
|
||||
return ret
|
||||
|
||||
def store_data(self, conf, data, insecure=False):
|
||||
|
|
|
|||
|
|
@ -169,9 +169,11 @@ def cli_settings(server=None, client=None, conf=None):
|
|||
pass
|
||||
client = client or request.args.get('client')
|
||||
server = server or request.args.get('serverName')
|
||||
template = request.args.get('template') or False
|
||||
return render_template(
|
||||
'settings.html',
|
||||
settings=True,
|
||||
template=template,
|
||||
client=client,
|
||||
server=server,
|
||||
conf=conf,
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@
|
|||
* }
|
||||
* 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']);
|
||||
|
||||
|
|
@ -104,7 +105,7 @@ app.config(function(uiSelectConfig) {
|
|||
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.strings = [];
|
||||
$scope.clients = [];
|
||||
|
|
@ -120,33 +121,44 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
|
|||
$scope.revokeEnabled = false;
|
||||
$scope.inc_invalid = {};
|
||||
$scope.old = {};
|
||||
$scope.raw = {};
|
||||
$scope.spy = {};
|
||||
$scope.new = {
|
||||
'bools': undefined,
|
||||
'integers': undefined,
|
||||
'strings': undefined,
|
||||
'multis': undefined
|
||||
'multis': undefined,
|
||||
'templates': undefined
|
||||
};
|
||||
$scope.add = {
|
||||
'bools': false,
|
||||
'integers': false,
|
||||
'strings': false,
|
||||
'multis': false
|
||||
'multis': false,
|
||||
'templates': false
|
||||
};
|
||||
$scope.changed = false;
|
||||
$scope.checkbox_translation = {
|
||||
'yes': "{{ _('yes') }}",
|
||||
'no': "{{ _('no') }}",
|
||||
'reset': "{{ _('Reset list') }}",
|
||||
'reset': "{{ _('reset list') }}",
|
||||
};
|
||||
$scope.dtOptions = {
|
||||
{{ macros.translate_datatable() }}
|
||||
{{ macros.get_page_length() }}
|
||||
};
|
||||
$scope.dtOptions = DTOptionsBuilder.newOptions();
|
||||
$scope.dtColumnDefs = [
|
||||
DTColumnDefBuilder.newColumnDef(0),
|
||||
DTColumnDefBuilder.newColumnDef(1),
|
||||
DTColumnDefBuilder.newColumnDef(2).notSortable(),
|
||||
];
|
||||
$scope.loadConfig = function() {
|
||||
{% if client -%}
|
||||
{% if template -%}
|
||||
$http.get('{{ url_for("api.client_settings", client=client, conf=conf, template=True, server=server) }}', { headers: { 'X-From-UI': true } })
|
||||
{% else -%}
|
||||
$http.get('{{ url_for("api.client_settings", client=client, conf=conf, server=server) }}', { headers: { 'X-From-UI': true } })
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
$http.get('{{ url_for("api.server_settings", conf=conf, server=server) }}', { headers: { 'X-From-UI': true } })
|
||||
{% endif -%}
|
||||
|
|
@ -160,7 +172,6 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
|
|||
$scope.all.integers = data.integer;
|
||||
$scope.multis = data.results.multi;
|
||||
$scope.all.multis = data.multi;
|
||||
$scope.clients = data.results.clients;
|
||||
$scope.server_doc = data.server_doc;
|
||||
$scope.suggest = data.suggest;
|
||||
$scope.placeholders = data.placeholders;
|
||||
|
|
@ -168,6 +179,7 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
|
|||
$scope.includes = data.results.includes;
|
||||
$scope.includes_ori = angular.copy($scope.includes);
|
||||
$scope.includes_ext = data.results.includes_ext;
|
||||
$scope.templates = data.results.templates;
|
||||
$scope.hierarchy = data.results.hierarchy;
|
||||
$scope.refreshHierarchy();
|
||||
$scope.refreshScrollspy();
|
||||
|
|
@ -177,6 +189,7 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
|
|||
notifAll(response.data);
|
||||
$('#waiting-container').hide();
|
||||
});
|
||||
};
|
||||
$http.get('{{ url_for("api.setting_options", server=server) }}', { headers: { 'X-From-UI': true } })
|
||||
.then(function(response) {
|
||||
$scope.revokeEnabled = response.data.is_revocation_enabled;
|
||||
|
|
@ -345,14 +358,24 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
|
|||
$scope.new[type] = undefined;
|
||||
}
|
||||
$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');
|
||||
diff = _.difference($scope.all[type], keys);
|
||||
diff = _.difference(all, keys);
|
||||
$scope.avail[type] = [];
|
||||
_(diff).forEach(function(n) {
|
||||
v = $scope.defaults[n];
|
||||
if (!v && type == 'multis') {
|
||||
if (!v && type === 'multis') {
|
||||
v = [''];
|
||||
}
|
||||
if (!v && type === 'templates') {
|
||||
v = $scope.all[type][n];
|
||||
}
|
||||
$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.new.multis = false;
|
||||
$scope.changed = true;
|
||||
$scope.refreshScrollspy();
|
||||
};
|
||||
$scope.addMulti = function(pindex) {
|
||||
$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.includes.splice(index, 1);
|
||||
$scope.includes_ori.splice(index, 1);
|
||||
$scope.changed = true;
|
||||
$scope.refreshScrollspy();
|
||||
};
|
||||
$scope.clickAddIncludes = function() {
|
||||
val = '';
|
||||
|
|
@ -398,6 +424,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
|
|||
}
|
||||
$scope.includes.push(val);
|
||||
$scope.includes_ori.push(val2);
|
||||
$scope.changed = true;
|
||||
$scope.refreshScrollspy();
|
||||
};
|
||||
$scope.select = function(selected, select, type) {
|
||||
select.search = undefined;
|
||||
|
|
@ -405,6 +433,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
|
|||
selected.value = $scope.old[type][selected.name];
|
||||
}
|
||||
$scope[type].push(selected);
|
||||
console.log(selected);
|
||||
console.log($scope[type]);
|
||||
$scope.add[type] = false;
|
||||
$scope.changed = true;
|
||||
};
|
||||
|
|
@ -458,19 +488,46 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$scrollspy', function($scope,
|
|||
};
|
||||
$scope.getClientsList = function() {
|
||||
api = '{{ url_for("api.clients_list", server=server) }}';
|
||||
$.ajax({
|
||||
url: api,
|
||||
type: 'GET'
|
||||
}).done(function(data) {
|
||||
$http.get(
|
||||
api,
|
||||
{
|
||||
headers: { 'X-From-UI': true },
|
||||
}
|
||||
).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() {
|
||||
api = '{{ url_for("api.client_settings", client=client, server=server) }}';
|
||||
$.ajax({
|
||||
url: api,
|
||||
type: 'DELETE',
|
||||
{% if template -%}
|
||||
data: { template: true }
|
||||
{% else -%}
|
||||
data: { delcert: $('#delcert').is(':checked'), revoke: $('#revoke').is(':checked'), keepconf: $('#keepconf').is(':checked') }
|
||||
{% endif -%}
|
||||
})
|
||||
.fail(myFail)
|
||||
.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 */
|
||||
$scope.focusIn = function(ev) {
|
||||
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.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 () {
|
||||
$('#config-nav a').click(function (e) {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -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-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-datatables-0.6.2/dist/angular-datatables.min.js') }}"></script>
|
||||
{% endif -%}
|
||||
{% if tree or settings -%}
|
||||
<!-- Fancytree Javascript
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
<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="#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>
|
||||
<div id="config-tab-content" class="tab-content">
|
||||
<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>
|
||||
{% 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>
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
<form class="form-horizontal" action="{{ url_for('api.server_settings', conf=conf, server=server) }}" method="POST" ng-submit="submit($event)" name="setSettings" onbeforeunload>
|
||||
{% endif -%}
|
||||
|
|
@ -263,13 +268,59 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
{% 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">
|
||||
<button type="button" class="btn btn-success btn-primary" ng-click="clickAddIncludes()"><span class="glyphicon glyphicon-plus"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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="col-lg-3 col-lg-offset-3">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="!setSettings.$valid">{{ _('Save') }}</button>
|
||||
|
|
@ -278,11 +329,14 @@
|
|||
{% if client and not conf -%}
|
||||
<div class="btn-group dropupi text-right">
|
||||
<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>
|
||||
<ul class="dropdown-menu browse">
|
||||
<li><label for="keepconf">{{ _('Do not remove the configuration:') }} </label><input type="checkbox" id="keepconf" name="keepconf"></li>
|
||||
<li><label for="delcert">{{ _('Remove associated certificate:') }} </label><input type="checkbox" id="delcert" name="delcert"></li>
|
||||
<li><label for="revoke">{{ _('Revoke associated certificate:') }} </label><input type="checkbox" id="revoke" name="revoke" ng-disabled="!revokeEnabled"></li>
|
||||
</ul>
|
||||
{% endif -%}
|
||||
</div>
|
||||
{% endif -%}
|
||||
</div>
|
||||
|
|
@ -305,7 +359,7 @@
|
|||
<div class="tab-pane fade" id="clients">
|
||||
<div style="padding-top: 80px; margin-top: -45px;"></div>
|
||||
<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>
|
||||
<tr>
|
||||
<th>{{ _('Name') }}</th><th>{{ _('Path') }}</th><th></th>
|
||||
|
|
@ -317,11 +371,58 @@
|
|||
<td>{{ client.name }}</td>
|
||||
<td>{{ client.value }}</td>
|
||||
{% 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"> 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>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -7,18 +7,8 @@
|
|||
<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="#includes_source"><a class="scroll" href="#includes_source">{{ _('Source files') }}</a></li>
|
||||
</ul>
|
||||
<h4>{{ _('Client to configure') }}</h4>
|
||||
<ul class="nav nav-sidebar">
|
||||
<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>
|
||||
{% if client and not template -%}
|
||||
<li data-target="#templates"><a class="scroll" href="#templates">{{ _('Templates') }}</a></li>
|
||||
{% endif -%}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ v0.6.0
|
|||
|
||||
- **New** - WebSocket support for better/smarter notifications.
|
||||
|
||||
- **New** - Client configuration templates.
|
||||
|
||||
v0.5.0
|
||||
------
|
||||
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -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.tpl.min.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/locale/fr.js',
|
||||
'burpui/static/vendor/moment/locale/es.js',
|
||||
|
|
|
|||
|
|
@ -140,7 +140,6 @@ class BurpuiAPITestCase(TestCase):
|
|||
u'multi': [],
|
||||
u'includes': [],
|
||||
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}],
|
||||
}
|
||||
),
|
||||
|
|
@ -167,8 +166,8 @@ class BurpuiAPITestCase(TestCase):
|
|||
u'multi': [],
|
||||
u'includes': [],
|
||||
u'includes_ext': [],
|
||||
u'clients': [],
|
||||
u'hierarchy': [],
|
||||
u'templates': [],
|
||||
}
|
||||
),
|
||||
(u'boolean', self.bui.client.get_parser_attr('boolean_cli')),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue