Commits (30)
......@@ -80,6 +80,10 @@ Notes
Please feel free to report any issues on my `gitlab <https://git.ziirish.me/ziirish/burp-ui/issues>`_
I have closed the *github tracker* to have a unique tracker system.
The multi-server mode is a **Work In Progress**, it is quite unstable yet. Use
it only if you know what you are doing.
TODO
----
......@@ -91,6 +95,11 @@ But I didn't think yet of what to do.
Changelog
---------
* version 0.0.5:
- Add multi-server support
- fix bugs
* version 0.0.4:
- Add the ability to download files directly from the web interface
......@@ -123,6 +132,7 @@ But this project is built on top of other tools listed here:
- `bootstrap <http://getbootstrap.com/>`_ (`MIT <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/bootstrap/LICENSE>`__)
- `typeahead <http://twitter.github.io/typeahead.js/>`_ (`MIT <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/typeahead/LICENSE>`__)
- `bootswatch <http://bootswatch.com/>`_ (`MIT <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/bootstrap/bootswatch.LICENSE>`__)
- Home-made `favicon <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/images/favicon.ico>`_ based on pictures from `simsoncrazy <http://www.simpsoncrazy.com/pictures/homer>`_
Also note that this project is made with the Awesome `Flask`_ micro-framework.
......
#!/usr/bin/env python
# -*- coding: utf8 -*-
import sys
import os
import logging
from optparse import OptionParser
sys.path.append('{0}/..'.format(os.path.join(os.path.dirname(os.path.realpath(__file__)))))
from burpui.agent import BUIAgent as Agent
if __name__ == '__main__':
"""
Main function
"""
parser = OptionParser()
parser.add_option('-v', '--verbose', dest='log', help='verbose output', action='store_true')
parser.add_option('-c', '--config', dest='config', help='configuration file', metavar='CONFIG')
(options, args) = parser.parse_args()
if options.config:
if os.path.isfile(options.config):
conf = options.config
else:
raise IOError('File not found: \'{0}\''.format(options.config))
else:
conf_files = ['/etc/burp/buiagent.cfg', os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'share', 'burpui', 'etc', 'buiagent.cfg')]
for p in conf_files:
if os.path.isfile(p):
conf = p
break
agent = Agent(conf, options.log)
agent.run()
......@@ -20,6 +20,8 @@ if __name__ == '__main__':
(options, args) = parser.parse_args()
d = options.log
app.config['DEBUG'] = d
if d:
app.config['TESTING'] = True
if options.config:
if os.path.isfile(options.config):
......
bin/bui-agent
\ No newline at end of file
share/burpui/etc/buiagent.cfg
\ No newline at end of file
......@@ -10,7 +10,7 @@ __license__ = 'BSD 3-clause'
from flask import Flask
from flask.ext.login import LoginManager
from burpui.server import Server as BurpUI
from burpui.server import BUIServer as BurpUI
# First, we setup the app
app = Flask(__name__)
......
# -*- coding: utf8 -*-
import os
import sys
import struct
import json
import time
import ConfigParser
import SocketServer
from threading import Thread
g_port = 10000
g_bind = '::'
g_ssl = False
g_version = 1
g_sslcert = ''
g_sslkey = ''
g_password = 'password'
class BUIAgent:
def __init__(self, conf=None, debug=False):
global g_port, g_bind, g_ssl, g_version, g_sslcert, g_sslkey, g_password
self.conf = conf
self.dbg = debug
print 'conf: '+self.conf
print 'debug: '+str(self.dbg)
if not conf:
raise IOError('No configuration file found')
config = ConfigParser.ConfigParser({'port': g_port,'bind': g_bind,
'ssl': g_ssl, 'sslcert': g_sslcert, 'sslkey': g_sslkey,
'version': g_version, 'password': g_password})
with open(conf) as fp:
config.readfp(fp)
try:
self.port = config.getint('Global', 'port')
self.bind = config.get('Global', 'bind')
self.vers = config.getint('Global', 'version')
try:
self.ssl = config.getboolean('Global', 'ssl')
except ValueError:
self.app.logger.error("Wrong value for 'ssl' key! Assuming 'false'")
self.ssl = False
self.sslcert = config.get('Global', 'sslcert')
self.sslkey = config.get('Global', 'sslkey')
self.password = config.get('Global', 'password')
except ConfigParser.NoOptionError, e:
raise e
module = 'burpui.misc.backend.burp{0}'.format(self.vers)
try:
mod = __import__(module, fromlist=['Burp'])
Client = mod.Burp
self.backend = Client(conf=conf)
except Exception, e:
self.app.logger.error('Failed loading backend for Burp version %d: %s', self.vers, str(e))
sys.exit(2)
self.methods = {
'status': self.backend.status,
'parse_backup_log': self.backend.parse_backup_log,
'get_counters': self.backend.get_counters,
'is_backup_running': self.backend.is_backup_running,
'is_one_backup_running': self.backend.is_one_backup_running,
'get_all_clients': self.backend.get_all_clients,
'get_client': self.backend.get_client,
'get_tree': self.backend.get_tree,
'restore_files': self.backend.restore_files
}
self.server = AgentServer((self.bind, self.port), AgentTCPHandler, self)
def run(self):
try:
self.server.serve_forever()
except KeyboardInterrupt:
sys.exit(0)
def debug(self, msg, *args):
if self.dbg:
print msg % (args)
class AgentTCPHandler(SocketServer.BaseRequestHandler):
"One instance per connection. Override handle(self) to customize action."
def handle(self):
# self.request is the client connection
self.server.agent.debug('===============>')
try:
lengthbuf = self.request.recv(8)
length, = struct.unpack('!Q', lengthbuf)
data = self.recvall(length)
self.server.agent.debug('####################')
self.server.agent.debug('recv: %s', data)
self.server.agent.debug('####################')
j = json.loads(data)
if j['password'] != self.server.agent.password:
self.server.agent.debug('-----> Wrong Password <-----')
self.request.sendall('KO')
return
if j['func'] not in self.server.agent.methods:
self.server.agent.debug('-----> Wrong method <-----')
self.request.sendall('KO')
return
self.request.sendall('OK')
if j['func'] == 'restore_files':
res = self.server.agent.methods[j['func']](**j['args'])
else:
if j['args']:
res = json.dumps(self.server.agent.methods[j['func']](**j['args']))
else:
res = json.dumps(self.server.agent.methods[j['func']]())
self.server.agent.debug('####################')
self.server.agent.debug('result: %s', res)
self.server.agent.debug('####################')
if j['func'] == 'restore_files':
size = os.path.getsize(res)
self.request.sendall(struct.pack('!Q', size))
with open(res, 'rb') as f:
buf = f.read(1024)
while buf:
self.server.agent.debug('sending %d Bytes', len(buf))
self.request.sendall(buf)
buf = f.read(1024)
else:
self.request.sendall(struct.pack('!Q', len(res)))
self.request.sendall(res)
self.request.close()
except Exception, e:
self.server.agent.debug('ERROR: %s', str(e))
finally:
self.server.agent.debug('<===============')
def recvall(self, length=1024):
buf = b''
bsize = 1024
received = 0
if length < bsize:
bsize = length
while received < length:
newbuf = self.request.recv(bsize)
if not newbuf:
time.sleep(0.1)
continue
buf += newbuf
received += len(newbuf)
return buf
class AgentServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
# Ctrl-C will cleanly kill all spawned threads
daemon_threads = True
# much faster rebinding
allow_reuse_address = True
def __init__(self, server_address, RequestHandlerClass, agent=None):
self.agent = agent
SocketServer.TCPServer.__init__(self, server_address, RequestHandlerClass)
def get_request(self):
if self.agent.ssl:
import ssl
(newsocket, fromaddr) = SocketServer.TCPServer.get_request(self)
connstream = ssl.wrap_socket(newsocket,
server_side=True,
certfile=self.agent.sslcert,
keyfile=self.agent.sslkey,
ssl_version=ssl.PROTOCOL_SSLv23)
return connstream, fromaddr
# if we don't use ssl, use the 'super' method
return SocketServer.TCPServer.get_request(self)
......@@ -57,8 +57,8 @@ class LdapLoader:
query = 'uid={0}'.format(uid)
self.app.logger.info('query: %s | base: %s', query, self.base)
r = self.ldap.search(query, base_dn=self.base)
except:
self.app.logger.info('Ooops, LDAP lookup failed')
except Exception, e:
self.app.logger.error('Ooops, LDAP lookup failed: %s', str(e))
return None
return r[0]['uid'][0]
......
......@@ -9,14 +9,16 @@ import ConfigParser
import shutil
import subprocess
import zipfile
import codecs
from burpui.misc.utils import human_readable as _hr
from burpui.misc.backend.interface import BUIbackend, BUIserverException
g_burpport = 4972
g_burphost = '127.0.0.1'
g_tmpdir = '/tmp/buirestore'
g_tmpdir = u'/tmp/buirestore'
g_burpbin = '/usr/sbin/burp'
g_stripbin = '/usr/sbin/vss_strip'
class Burp(BUIbackend):
states = {
......@@ -60,61 +62,90 @@ class Burp(BUIbackend):
]
def __init__(self, app=None, conf=None):
global g_burpport, g_burphost, g_tmpdir, g_burpbin
global g_burpport, g_burphost, g_tmpdir, g_burpbin, g_stripbin
self.app = app
self.host = g_burphost
self.port = g_burpport
self.burpbin = g_burpbin
self.stripbin = g_stripbin
self.tmpdir = g_tmpdir
self.running = []
if conf:
config = ConfigParser.ConfigParser({'bport': g_burpport, 'tmpdir': g_tmpdir, 'burpbin': g_burpbin})
with open(conf) as fp:
config = ConfigParser.ConfigParser({'bport': g_burpport, 'bhost': g_burphost, 'tmpdir': g_tmpdir, 'burpbin': g_burpbin, 'stripbin': g_stripbin})
with codecs.open(conf, 'r', 'utf-8') as fp:
config.readfp(fp)
try:
self.port = config.getint('Burp1', 'bport')
self.host = config.get('Burp1', 'bhost')
tdir = config.get('Burp1', 'tmpdir')
bbin = config.get('Burp1', 'burpbin')
strip = config.get('Burp1', 'stripbin')
if self.host not in ['127.0.0.1', '::1']:
self.logger('warning', "Invalid value for 'bhost'. Must be '127.0.0.1' or '::1'. Falling back to '%s'", g_burphost)
self.host = g_burphost
if not strip.startswith('/'):
self.logger('warning', "Please provide an absolute path for the 'stripbin' option. Fallback to '%s'", g_stripbin)
strip = g_stripbin
elif not re.match('^\S+$', strip):
self.logger('warning', "Incorrect value for the 'stripbin' option. Fallback to '%s'", g_stripbin)
strip = g_stripbin
elif not os.path.isfile(strip) or not os.access(strip, os.X_OK):
self.logger('warning', "'%s' does not exist or is not executable. Fallback to '%s'", strip, g_stripbin)
strip = g_stripbin
if not bbin.startswith('/'):
self.app.logger.warning('Please provide an absolute path for the \'burpbin\' option. Fallback to \'%s\'', g_burpbin)
self.logger('warning', "Please provide an absolute path for the 'burpbin' option. Fallback to '%s'", g_burpbin)
bbin = g_burpbin
elif not re.match('^\S+$', bbin):
self.app.logger.warning('Incorrect value for the \'burpbin\' option. Fallback to \'%s\'', g_burpbin)
self.logger('warning', "Incorrect value for the 'burpbin' option. Fallback to '%s'", g_burpbin)
bbin = g_burpbin
elif not os.path.isfile(bbin) or not os.access(bbin, os.X_OK):
self.app.logger.warning('\'%s\' does not exist or is not executable. Fallback to \'%s\'', bbin, g_burpbin)
self.logger('warning', "'%s' does not exist or is not executable. Fallback to '%s'", bbin, g_burpbin)
bbin = g_burpbin
if not tdir.startswith('/'):
self.app.logger.warning('Please provide an absolute path for the \'tmpdir\' option. Fallback to \'%s\'', g_tmpdir)
self.logger('warning', "Please provide an absolute path for the 'tmpdir' option. Fallback to '%s'", g_tmpdir)
tdir = g_tmpdir
elif not re.match('^\S+$', tdir):
self.app.logger.warning('Incorrect value for the \'tmpdir\' option. Fallback to \'%s\'', g_tmpdir)
self.logger('warning', "Incorrect value for the 'tmpdir' option. Fallback to '%s'", g_tmpdir)
tdir = g_tmpdir
elif os.path.isdir(tdir) and os.listdir(tdir):
raise Exception('\'{0}\' is not empty!'.format(tdir))
elif os.path.isdir(tdir) and os.listdir(tdir) and not self.app.config.get('TESTING'):
raise Exception("'{0}' is not empty!".format(tdir))
elif os.path.isdir(tdir) and not os.access(tdir, os.W_OK|os.X_OK):
self.app.logger.warning('\'%s\' is not writable. Fallback to \'%s\'', tdir, g_tmpdir)
self.logger('warning', "'%s' is not writable. Fallback to '%s'", tdir, g_tmpdir)
tdir = g_tmpdir
self.burpbin = bbin
self.tmpdir = tdir
self.stripbin = strip
except ConfigParser.NoOptionError, e:
self.app.logger.error(str(e))
self.logger('error', str(e))
except ConfigParser.NoSectionError, e:
self.app.logger.error(str(e))
self.logger('error', str(e))
self.app.logger.info('burp port: %d', self.port)
self.app.logger.info('burp host: %s', self.host)
self.app.logger.info('burp binary: %s', self.burpbin)
self.app.logger.info('temporary dir: %s', self.tmpdir)
self.logger('info', 'burp port: %d', self.port)
self.logger('info', 'burp host: %s', self.host)
self.logger('info', 'burp binary: %s', self.burpbin)
self.logger('info', 'strip binary: %s', self.stripbin)
self.logger('info', 'temporary dir: %s', self.tmpdir)
def logger(self, level, *args):
if self.app:
logs = {
'info': self.app.logger.info,
'error': self.app.logger.error,
'debug': self.app.logger.debug,
'warning': self.app.logger.warning
}
if level in logs:
logs[level](*args)
"""
Utilities functions
"""
def status(self, query='\n'):
def status(self, query='\n', agent=None):
"""
status connects to the burp status port, ask the given 'question' and
parses the output in an array
......@@ -144,10 +175,10 @@ class Burp(BUIbackend):
f.close()
return r
except socket.error:
self.app.logger.error('Cannot contact burp server at %s:%s', self.host, self.port)
self.logger('error', 'Cannot contact burp server at %s:%s', self.host, self.port)
raise BUIserverException('Cannot contact burp server at {0}:{1}'.format(self.host, self.port))
def parse_backup_log(self, f, n, c=None):
def parse_backup_log(self, f, n, c=None, agent=None):
"""
parse_backup_log parses the log.gz of a given backup and returns a dict
containing different stats used to render the charts in the reporting view
......@@ -225,7 +256,7 @@ class Burp(BUIbackend):
for key, regex in lookup_complex.iteritems():
r = re.search(regex, line)
if r:
self.app.logger.debug("match[1]: '{0}'".format(r.group(1)))
self.logger('debug', "match[1]: '{0}'".format(r.group(1)))
sp = re.split('\s+', r.group(1))
backup[key] = {
'new': int(sp[0]),
......@@ -238,24 +269,28 @@ class Burp(BUIbackend):
break
return backup
def get_counters(self, name=None):
def get_counters(self, name=None, agent=None):
"""
get_counters parses the stats of the live status for a given client and
returns a dict
"""
r = {}
if not name or name not in self.running:
return r
if agent:
if not name or name not in self.running[agent]:
return r
else:
if not name or name not in self.running:
return r
f = self.status('c:{0}\n'.format(name))
if not f:
return r
for line in f:
self.app.logger.debug('line: {0}'.format(line))
self.logger('debug', 'line: {0}'.format(line))
rs = re.search('^{0}\s+(\d)\s+(\S)\s+(.+)$'.format(name), line)
if rs and rs.group(2) == 'r' and int(rs.group(1)) == 2:
c = 0
for v in rs.group(3).split('\t'):
self.app.logger.debug('{0}: {1}'.format(self.counters[c], v))
self.logger('debug', '{0}: {1}'.format(self.counters[c], v))
if c > 0 and c < 15:
val = map(int, v.split('/'))
if val[0] > 0 or val[1] > 0 or val[2] or val[3] > 0:
......@@ -279,7 +314,7 @@ class Burp(BUIbackend):
r['timeleft'] = -1
return r
def is_backup_running(self, name=None):
def is_backup_running(self, name=None, agent=None):
"""
is_backup_running returns True if the given client is currently running a
backup
......@@ -296,7 +331,7 @@ class Burp(BUIbackend):
return True
return False
def is_one_backup_running(self):
def is_one_backup_running(self, agent=None):
"""
is_one_backup_running returns a list of clients name that are currently
running a backup
......@@ -307,12 +342,12 @@ class Burp(BUIbackend):
except BUIserverException:
return r
for c in cls:
if self.is_backup_running(c['name']):
if self.is_backup_running(c['name'], agent):
r.append(c['name'])
self.running = r
return r
def get_all_clients(self):
def get_all_clients(self, agent=None):
"""
get_all_clients returns a list of dict representing each clients with their
name, state and last backup date
......@@ -320,14 +355,14 @@ class Burp(BUIbackend):
j = []
f = self.status()
for line in f:
self.app.logger.debug("line: '{0}'".format(line))
self.logger('debug', "line: '{0}'".format(line))
regex = re.compile('\s*(\S+)\s+\d\s+(\S)\s+(.+)')
m = regex.search(line)
c = {}
c['name'] = m.group(1)
c['state'] = self.states[m.group(2)]
infos = m.group(3)
self.app.logger.debug("infos: '{0}'".format(infos))
self.logger('debug', "infos: '{0}'".format(infos))
if infos == "0":
c['last'] = 'never'
elif re.match('^\d+\s\d+\s\d+$', infos):
......@@ -339,7 +374,7 @@ class Burp(BUIbackend):
j.append(c)
return j
def get_client(self, name=None):
def get_client(self, name=None, agent=None):
"""
get_client returns a list of dict representing the backups (with its number
and date) of a given client
......@@ -352,7 +387,7 @@ class Burp(BUIbackend):
for line in f:
if not re.match('^{0}\t'.format(c), line):
continue
self.app.logger.debug("line: '{0}'".format(line))
self.logger('debug', "line: '{0}'".format(line))
regex = re.compile('\s*(\S+)\s+\d\s+(\S)\s+(.+)')
m = regex.search(line)
if m.group(3) == "0" or m.group(2) not in [ 'i', 'c', 'C' ]:
......@@ -368,7 +403,7 @@ class Burp(BUIbackend):
r.reverse()
return r
def get_tree(self, name=None, backup=None, root=None):
def get_tree(self, name=None, backup=None, root=None, agent=None):
"""
get_tree returns a list of dict representing files/dir (with their attr)
within a given path
......@@ -384,7 +419,7 @@ class Burp(BUIbackend):
f = self.status('c:{0}:b:{1}:p:{2}\n'.format(name, backup, top))
useful = False
for line in f:
self.app.logger.debug("line: '{0}'".format(line))
self.logger('debug', "line: '{0}'".format(line))
if not useful and re.match('^-list begin-$', line):
useful = True
continue
......@@ -399,19 +434,19 @@ class Burp(BUIbackend):
t['type'] = 'd'
else:
t['type'] = 'f'
sp = re.split('\s+', line)
sp = re.split('\s+', line, 7)
t['mode'] = sp[0]
t['inodes'] = sp[1]
t['uid'] = sp[2]
t['gid'] = sp[3]
t['size'] = '{0:.1eM}'.format(_hr(sp[4]))
t['date'] = '{0} {1}'.format(sp[5], sp[6])
t['name'] = sp[len(sp)-1]
t['name'] = sp[7]
t['parent'] = top
r.append(t)
return r
def restore_files(self, name=None, backup=None, files=None):
def restore_files(self, name=None, backup=None, files=None, agent=None):
if not name or not backup or not files:
return None
flist = json.loads(files)
......@@ -419,27 +454,46 @@ class Burp(BUIbackend):
return None
if os.path.isdir(self.tmpdir):
shutil.rmtree(self.tmpdir)
full_reg = u''
for r in flist['restore']:
reg = ''
reg = u''
if r['folder'] and r['key'] != '/':
reg = r['key']+'/'
reg += '^'+r['key']+'/|'
else:
reg = r['key']
#cmd = self.burpbin+' -C '+name+' -a r -b '+str(backup)+' -r \''+reg+'\' -d '+self.tmpdir
status = subprocess.call([self.burpbin, '-C', name, '-a', 'r', '-b', str(backup), '-r', reg, '-d', self.tmpdir])
if status != 0:
return None
reg += '^'+r['key']+'$|'
full_reg += reg
cmd = [self.burpbin, '-C', name, '-a', 'r', '-b', str(backup), '-r', full_reg.rstrip('|'), '-d', self.tmpdir]
self.logger('debug', cmd)
status = subprocess.call(cmd)
if status != 0:
return None
zip_dir = self.tmpdir.rstrip(os.sep)
zip_file = zip_dir+'.zip'
if os.path.isfile(zip_file):
os.remove(zip_file)
zip_len = len(zip_dir) + 1
stripping = True
with zipfile.ZipFile(zip_file, mode='w', compression=zipfile.ZIP_DEFLATED) as zf:
for dirname, subdirs, files in os.walk(zip_dir):
for filename in files:
path = os.path.join(dirname, filename)
if stripping and os.path.isfile(path):
self.logger('debug', "stripping file: %s", path)
shutil.move(path, path+'.tmp')
status = subprocess.call([self.stripbin, '-i', path+'.tmp', '-o', path])
if status != 0:
os.remove(path)
shutil.move(path+'.tmp', path)
stripping = False
self.logger('debug', "Disable stripping since this file does not seem to embed VSS headers")
else:
os.remove(path+'.tmp')
entry = path[zip_len:]
zf.write(path, entry)
shutil.rmtree(self.tmpdir)
return zip_file
......@@ -6,28 +6,28 @@ class BUIbackend:
self.host = host
self.port = port
def status(self, query='\n'):
def status(self, query='\n', agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def parse_backup_log(self, f, n, c=None):
def parse_backup_log(self, f, n, c=None, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def get_counters(self, name=None):
def get_counters(self, name=None, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def is_backup_running(self, name=None):
def is_backup_running(self, name=None, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def is_one_backup_running(self):
def is_one_backup_running(self, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def get_all_clients(self):
def get_all_clients(self, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def get_client(self, name=None):
def get_client(self, name=None, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def get_tree(self, name=None, backup=None, root=None):
def get_tree(self, name=None, backup=None, root=None, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
class BUIserverException(Exception):
......
# -*- coding: utf8 -*-
import re
import copy
import socket
import sys
import json
import time
import struct
import ConfigParser
from burpui.misc.backend.interface import BUIbackend, BUIserverException
class Burp(BUIbackend):
def __init__(self, app=None, conf=None):
self.app = app
self.servers = {}
self.app.config['SERVERS'] = []
self.running = {}
if conf:
config = ConfigParser.ConfigParser()
with open(conf) as fp:
config.readfp(fp)
for sec in config.sections():
r = re.match('^Agent:(.+)$', sec)
if r:
try:
host = config.get(sec, 'host')
port = config.getint(sec, 'port')
password = config.get(sec, 'password')
ssl = config.getboolean(sec, 'ssl')
except Exception, e:
self.app.logger.error(str(e))
self.servers[r.group(1)] = NClient(app, host, port, password, ssl)
self.app.logger.debug(self.servers)
for key, serv in self.servers.iteritems():
self.app.config['SERVERS'].append(key)
"""
Utilities functions
"""
def status(self, query='\n', agent=None):
"""
status connects to the burp status port, ask the given 'question' and
parses the output in an array
"""
return self.servers[agent].status(query)
def parse_backup_log(self, f, n, c=None, agent=None):
"""
parse_backup_log parses the log.gz of a given backup and returns a dict
containing different stats used to render the charts in the reporting view
"""
return self.servers[agent].parse_backup_log(f, n, c)
def get_counters(self, name=None, agent=None):
"""
get_counters parses the stats of the live status for a given client and
returns a dict
"""
return self.servers[agent].get_counters(name)
def is_backup_running(self, name=None, agent=None):
"""
is_backup_running returns True if the given client is currently running a
backup
"""
return self.servers[agent].is_backup_running(name)
def is_one_backup_running(self, agent=None):
"""
is_one_backup_running returns a list of clients name that are currently
running a backup
"""
r = []
if agent:
r = self.servers[agent].is_one_backup_running(agent)
self.running[agent] = r
else:
r = {}
for a in self.servers:
r[a] = self.servers[a].is_one_backup_running(a)
self.running = r
return r
def get_all_clients(self, agent=None):
"""
get_all_clients returns a list of dict representing each clients with their
name, state and last backup date
"""
if agent not in self.servers:
return []
return self.servers[agent].get_all_clients()
def get_client(self, name=None, agent=None):
"""
get_client returns a list of dict representing the backups (with its number
and date) of a given client
"""
return self.servers[agent].get_client(name)
def get_tree(self, name=None, backup=None, root=None, agent=None):
"""
get_tree returns a list of dict representing files/dir (with their attr)
within a given path
"""
return self.servers[agent].get_tree(name, backup, root)
def restore_files(self, name=None, backup=None, files=None, agent=None):
return self.servers[agent].restore_files(name, backup, files)
class NClient(BUIbackend):
def __init__(self, app=None, host=None, port=None, password=None, ssl=None):
self.host = host
self.port = port
self.password = password
self.ssl = ssl
self.connected = False
self.app = app
def conn(self):
try:
if self.connected:
return
self.sock = self.do_conn()
self.connected = True
self.app.logger.debug('OK, connected to agent %s:%s', self.host, self.port)
except Exception, e:
self.connected = False
self.app.logger.error('Could not connect to %s:%s => %s', self.host, self.port, str(e))
def do_conn(self):
ret = None
if self.ssl:
import ssl
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ret = ssl.wrap_socket(s, cert_reqs=ssl.CERT_NONE, ssl_version=ssl.PROTOCOL_SSLv23)
try:
ret.connect((self.host, self.port))
except Exception, e:
self.app.logger.error('ERROR: %s', str(e))
raise e
else:
ret = socket.create_connection((self.host, self.port))
return ret
def ping(self):
self.conn()
res = self.connected
return res
def close(self, force=True):
if self.connected and force:
self.sock.close()
self.connected = False
def do_command(self, data=None):
self.conn()
res = '[]'
toclose = True
if not data:
return res
try:
data['password'] = self.password
raw = json.dumps(data)
length = len(raw)
self.sock.sendall(struct.pack('!Q', length))
self.sock.sendall(raw)
self.app.logger.debug("Sending: %s", raw)
tmp = self.recvall(2)
self.app.logger.debug("recv: '%s'", tmp)
if 'OK' != tmp:
self.app.logger.debug('Ooops, unsuccessful!')
return res
self.app.logger.debug("Data sent successfully")
lengthbuf = self.recvall(8, False)
length, = struct.unpack('!Q', lengthbuf)
if data['func'] == 'restore_files':
toclose = False
res = (self.sock, length)
else:
res = self.recvall(length)
except Exception, e:
self.app.logger.error(str(e))
finally:
self.close(toclose)
return res
def recvall(self, length=1024, debug=True, timeout=5):
buf = b''
bsize = 1024
received = 0
tries = 0
if length < bsize:
bsize = length
while received < length and tries < timeout:
newbuf = self.sock.recv(bsize)
if not newbuf:
tries += 1
time.sleep(0.1)
continue
buf += newbuf
received += len(newbuf)
if debug:
self.app.logger.debug('result (%d/%d): %s', len(buf), length, buf)
return buf
"""
Utilities functions
"""
def status(self, query='\n', agent=None):
"""
status connects to the burp status port, ask the given 'question' and
parses the output in an array
"""
data = {'func': 'status', 'args': {'query': query}}
return json.loads(self.do_command(data))
def parse_backup_log(self, f, n, c=None, agent=None):
"""
parse_backup_log parses the log.gz of a given backup and returns a dict
containing different stats used to render the charts in the reporting view
"""
data = {'func': 'parse_backup_log', 'args': {'f': f, 'n': n, 'c': c}}
return json.loads(self.do_command(data))
def get_counters(self, name=None, agent=None):
"""
get_counters parses the stats of the live status for a given client and
returns a dict
"""
data = {'func': 'get_counters', 'args': {'name': name}}
return json.loads(self.do_command(data))
def is_backup_running(self, name=None, agent=None):
"""
is_backup_running returns True if the given client is currently running a
backup
"""
data = {'func': 'is_backup_running', 'args': {'name': name}}
return json.loads(self.do_command(data))
def is_one_backup_running(self, agent=None):
"""
is_one_backup_running returns a list of clients name that are currently
running a backup
"""
data = {'func': 'is_one_backup_running', 'args': {'agent': agent}}
return json.loads(self.do_command(data))
def get_all_clients(self, agent=None):
"""
get_all_clients returns a list of dict representing each clients with their
name, state and last backup date
"""
data = {'func': 'get_all_clients', 'args': None}
return json.loads(self.do_command(data))
def get_client(self, name=None, agent=None):
"""
get_client returns a list of dict representing the backups (with its number
and date) of a given clientm
"""
data = {'func': 'get_client', 'args': {'name': name}}
return json.loads(self.do_command(data))
def get_tree(self, name=None, backup=None, root=None, agent=None):
"""
get_tree returns a list of dict representing files/dir (with their attr)
within a given path
"""
data = {'func': 'get_tree', 'args': {'name': name, 'backup': backup, 'root': root}}
return json.loads(self.do_command(data))
def restore_files(self, name=None, backup=None, files=None, agent=None):
data = {'func': 'restore_files', 'args': {'name': name, 'backup': backup, 'files': files}}
return self.do_command(data)
# -*- coding: utf8 -*-
import math
from zlib import adler32
from time import gmtime, strftime, time, sleep
from flask import Flask, request, render_template, jsonify, redirect, url_for, abort, flash, send_file
from flask import Flask, Response, request, render_template, jsonify, redirect, url_for, abort, flash, send_file
from flask.ext.login import login_user, login_required, logout_user
from werkzeug.datastructures import Headers
from burpui import app, bui, login_manager
from burpui.forms import LoginForm
......@@ -15,30 +18,68 @@ def load_user(userid):
return bui.uhandler.user(userid)
return None
@app.route('/test/download')
def test_download():
try:
resp = send_file('/tmp/monfichierr', as_attachment=True)
resp.set_cookie('fileDownload', 'true')
return resp
except Exception, e:
abort(500)
@app.route('/api/restore/<name>/<int:backup>', methods=['POST'])
@app.route('/api/<server>/restore/<name>/<int:backup>', methods=['POST'])
@login_required
def restore(name=None, backup=None):
def restore(server=None, name=None, backup=None):
l = request.form.get('list')
resp = None
if not l or not name or not backup:
abort(500)
archive = bui.cli.restore_files(name, backup, l)
if not archive:
abort(500)
try:
resp = send_file(archive, as_attachment=True)
resp.set_cookie('fileDownload', 'true')
return resp
except Exception, e:
abort(500)
if server:
filename = 'restoration_%d_%s_on_%s_at_%s.zip' % (backup, name, server, strftime("%Y-%m-%d_%H_%M_%S", gmtime()))
else:
filename = 'restoration_%d_%s_at_%s.zip' % (backup, name, strftime("%Y-%m-%d_%H_%M_%S", gmtime()))
if not server:
archive = bui.cli.restore_files(name, backup, l)
if not archive:
abort(500)
try:
resp = send_file(archive, as_attachment=True, attachment_filename=filename)
resp.set_cookie('fileDownload', 'true')
except Exception, e:
app.logger.error(str(e))
abort(500)
else:
socket = None
try:
socket, length = bui.cli.restore_files(name, backup, l, server)
app.logger.debug('Need to get %d Bytes : %s', length, socket)
def stream_file(sock, l):
bsize = 1024
timeout = 5
received = 0
tries = 0
if l < bsize:
bsize = l
while received < l and tries < timeout:
buf = b''
buf += sock.recv(bsize)
if not buf:
tries += 1
sleep(0.1)
continue
received += len(buf)
app.logger.debug('%d/%d', received, l)
yield buf
sock.close()
headers = Headers()
headers.add('Content-Disposition', 'attachment', filename=filename)
headers['Content-Length'] = length
resp = Response(stream_file(socket, length), mimetype='application/octet-stream',
headers=headers, direct_passthrough=True)
resp.set_cookie('fileDownload', 'true')
resp.set_etag('flask-%s-%s-%s' % (
time(),
length,
adler32(filename.encode('utf-8')) & 0xffffffff))
except Exception, e:
app.logger.error(str(e))
abort(500)
return resp
"""
Here is the API
......@@ -47,163 +88,228 @@ The whole API returns JSON-formated data
"""
@app.route('/api/running-clients.json')
@app.route('/api/<server>/running-clients.json')
@login_required
def running_clients():
def running_clients(server=None):
"""
API: running_clients
:returns: a list of running clients
"""
r = bui.cli.is_one_backup_running()
if not server:
server = request.args.get('server')
r = bui.cli.is_one_backup_running(server)
return jsonify(results=r)
@app.route('/api/render-live-template', methods=['GET'])
@app.route('/api/<server>/render-live-template', methods=['GET'])
@app.route('/api/render-live-template/<name>')
@app.route('/api/<server>/render-live-template/<name>')
@login_required
def render_live_tpl(name=None):
def render_live_tpl(server=None, name=None):
"""
API: render_live_tpl
:param name: the client name if any. You can also use the GET parameter
'name' to achieve the same thing
:returns: HTML that should be included directly into the page
"""
c = request.args.get('name')
if not name and not c:
abort(500)
if not server:
server = request.args.get('server')
if not name:
name = c
if name not in bui.cli.running:
abort(404)
name = request.args.get('name')
if not name:
abort(500)
if isinstance(bui.cli.running, dict):
if server and name not in bui.cli.running[server]:
abort(404)
else:
found = False
for k, a in bui.cli.running.iteritems():
found = found or (name in a)
if not found:
abort(404)
else:
if name not in bui.cli.running:
abort(404)
try:
counters = bui.cli.get_counters(name)
counters = bui.cli.get_counters(name, agent=server)
except BUIserverException:
counters = []
return render_template('live-monitor-template.html', cname=name, counters=counters)
return render_template('live-monitor-template.html', cname=name, counters=counters, server=server)
@app.route('/api/servers.json')
@login_required
def servers_json():
r = []
for serv in bui.cli.servers:
r.append({'name': serv, 'clients': len(bui.cli.servers[serv].get_all_clients(serv)), 'alive': bui.cli.servers[serv].ping()})
return jsonify(results=r)
@app.route('/api/live.json')
@app.route('/api/<server>/live.json')
@login_required
def live():
def live(server=None):
"""
API: live
:returns: the live status of the server
"""
if not server:
server = request.args.get('server')
r = []
for c in bui.cli.is_one_backup_running():
s = {}
s['client'] = c
try:
s['status'] = bui.cli.get_counters(c)
except BUIserverException:
s['status'] = []
r.append(s)
if server:
l = (bui.cli.is_one_backup_running(server))[server]
else:
l = bui.cli.is_one_backup_running()
if isinstance(l, dict):
for k, a in l.iteritems():
for c in a:
s = {}
s['client'] = c
s['agent'] = k
try:
s['status'] = bui.cli.get_counters(c, agent=k)
except BUIserverException:
s['status'] = []
r.append(s)
else:
for c in l:
s = {}
s['client'] = c
try:
s['status'] = bui.cli.get_counters(c, agent=server)
except BUIserverException:
s['status'] = []
r.append(s)
return jsonify(results=r)
@app.route('/api/running.json')
@app.route('/api/<server>/running.json')
@login_required
def backup_running():
def backup_running(server=None):
"""
API: backup_running
:returns: true if at least one backup is running
"""
j = bui.cli.is_one_backup_running()
r = len(j) > 0
j = bui.cli.is_one_backup_running(server)
r = False
if isinstance(j, dict):
for k, v in j.iteritems():
r = r or (len(v) > 0)
else:
r = len(j) > 0
return jsonify(results=r)
@app.route('/api/client-tree.json/<name>/<int:backup>', methods=['GET'])
@app.route('/api/<server>/client-tree.json/<name>/<int:backup>', methods=['GET'])
@login_required
def client_tree(name=None, backup=None):
def client_tree(server=None, name=None, backup=None):
"""
WebService: return a specific client files tree
:param name: the client name (mandatory)
:param backup: the backup number (mandatory)
"""
if not server:
server = request.args.get('server')
j = []
if not name or not backup:
return jsonify(results=j)
root = request.args.get('root')
try:
j = bui.cli.get_tree(name, backup, root)
j = bui.cli.get_tree(name, backup, root, agent=server)
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
return jsonify(results=j)
@app.route('/api/clients-report.json')
@app.route('/api/<server>/clients-report.json')
@login_required
def clients_report_json():
def clients_report_json(server=None):
"""
WebService: return a JSON with global stats
"""
if not server:
server = request.args.get('server')
j = []
try:
clients = bui.cli.get_all_clients()
clients = bui.cli.get_all_clients(agent=server)
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
cl = []
ba = []
for c in clients:
client = bui.cli.get_client(c['name'])
client = bui.cli.get_client(c['name'], agent=server)
if not client:
continue
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(c['name'], client[-1]['number']))
cl.append( { 'name': c['name'], 'stats': bui.cli.parse_backup_log(f, client[-1]['number']) } )
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(c['name'], client[-1]['number']), agent=server)
cl.append( { 'name': c['name'], 'stats': bui.cli.parse_backup_log(f, client[-1]['number'], agent=server) } )
for b in client:
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(c['name'], b['number']))
ba.append(bui.cli.parse_backup_log(f, b['number'], c['name']))
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(c['name'], b['number']), agent=server)
ba.append(bui.cli.parse_backup_log(f, b['number'], c['name'], agent=server))
j.append( { 'clients': cl, 'backups': sorted(ba, key=lambda k: k['end']) } )
return jsonify(results=j)
@app.route('/api/client-stat.json/<name>')
@app.route('/api/<server>/client-stat.json/<name>')
@app.route('/api/client-stat.json/<name>/<int:backup>')
@app.route('/api/<server>/client-stat.json/<name>/<int:backup>')
@login_required
def client_stat_json(name=None, backup=None):
def client_stat_json(server=None, name=None, backup=None):
"""
WebService: return a specific client detailed report
"""
if not server:
server = request.args.get('server')
j = []
if not name:
err = [[1, 'No client defined']]
return jsonify(notif=err)
if backup:
try:
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(name, backup))
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(name, backup), agent=server)
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
j = bui.cli.parse_backup_log(f, backup)
j = bui.cli.parse_backup_log(f, backup, agent=server)
else:
try:
cl = bui.cli.get_client(name)
cl = bui.cli.get_client(name, agent=server)
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
for c in cl:
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(name, c['number']))
j.append(bui.cli.