Skip to content
Commits on Source (95)
......@@ -2,4 +2,6 @@
burpui-dev.cfg
devel.sh
*.egg*
.coverage
dist
py2.7
include LICENSE
include README.md
include README.rst
include VERSION
include requirements.txt
include test-requirements.txt
include share/burpui/etc/burpui.cfg
include share/burpui/etc/buiagent.cfg
include contrib/debian/init.sh
include contrib/centos/init.sh
recursive-include burpui *
Build Status
------------
.. image:: http://ci.ziirish.me/projects/1/status.png?ref=master
:target: http://ci.ziirish.me/projects/1?ref=master
.. image:: https://ci.ziirish.me/projects/1/status.png?ref=master
:target: https://ci.ziirish.me/projects/1?ref=master
Screenshots
-----------
.. image:: https://raw.githubusercontent.com/ziirish/burp-ui/master/pictures/burp-ui.gif
:target: https://git.ziirish.me/ziirish/burp-ui/blob/master/pictures/burp-ui.gif
What's that?
------------
Let me introduce you ``Burp-UI``. It is a web-based UI to manage your
burp-servers.
You can view different reports about burp-servers, burp-clients, backups, etc.
``Burp-UI`` allows you to perform *on-the-fly* restorations and should allow
you to edit/manage your burp-server's conf file very soon.
It is actually an improvement of the burp status monitor (``burp -c /etc/burp/burp-server.conf -a s``).
It currently supports only the burp-1.x branch but it is totally modular so
supporting burp-2.x won't be a big deal.
So in order to work properly, you must be running ``Burp-UI`` on the same host
that runs your burp-server (because the burp status port only listen on
*localhost*).
If you don't want to, I developed a ``bui-agent`` that allows you to *proxify*
external commands to your burp status port.
Who are you?
------------
I'm `Ziirish <http://ziirish.info>`_, a French sysadmin that loves `Burp`_ and
would like to help its adoption by providing it a nice and powerful interface.
If you like my work, you can:
* Thank me by sending me an email or writing a nice comment
* Buy me a beer or some fries or both!
* Make a donation on my Paypal
Contributing
------------
Contributions are welcome. You can help in any way you want, for instance by
opening issues on the `bug tracker <https://git.ziirish.me/ziirish/burp-ui/issues>`__,
sending patches, etc.
There is also a dedicated website. Currently it only hosts a `Discourse <http://www.discourse.org/>`__
instance where you ca discuss with each other.
Feel free to use it and post your tips and remarks.
The address is: `http://burpui.ziirish.me/ <http://burpui.ziirish.me/>`__
Requirements
------------
......@@ -29,7 +80,7 @@ Then we install the module itself:
Installation
------------
Burp-UI is written in Python with the `Flask`_ micro-framework.
``Burp-UI`` is written in Python with the `Flask`_ micro-framework.
The easiest way to install Flask is to use ``pip``.
On Debian, you can install ``pip`` with the following command:
......@@ -58,11 +109,54 @@ By default, ``burp-ui`` listens on all interfaces (including IPv6) on port 5000.
You can then point your browser to http://127.0.0.1:5000/
Development
-----------
If you wish to use the latest and yet unstable version (eg. `master <https://git.ziirish.me/ziirish/burp-ui/tree/master>`__),
you can install it using ``pip`` too, but I would recommend you to use a
``virtualenv``.
To do so, run the following commands:
::
mkdir /opt/bui-venv
pip install virtualenv
virtualenv /opt/bui-venv
source /opt/bui-venv/bin/activate
pip install git+https://git.ziirish.me/ziirish/burp-ui.git
You can uninstall/disable this ``Burp-UI`` setup by typing ``deactivate`` and
removing the ``/opt/bui-venv`` directory.
Gunicorn
--------
Starting from v0.0.6, ``Burp-UI`` supports `Gunicorn <http://gunicorn.org>`_ in
order to handle multiple users simultaneously.
You need to install ``gunicorn`` and ``eventlet``:
::
pip install eventlet
pip install gunicorn
You will then be able to launch ``Burp-UI`` this way:
::
gunicorn -k eventlet -w 4 'burpui:init(conf="/path/to/burpui.cfg")'
Instructions
------------
In order to make the *on the fly* restoration/download functionality work, there
you need to check a few things:
In order to make the *on the fly* restoration/download functionality work, you
need to check a few things:
1. Provide the full path of the burp (client) binary file
2. Provide the full path of an empty directory where a temporary restoration
......@@ -74,45 +168,79 @@ you need to check a few things:
restore files of other clients (option *restore_client* in burp-server
configuration)
Troubleshooting
---------------
In case you encounter troubles with ``Burp-UI``, you should run it with the
``-d`` flag and paste the relevant output within your bug-report.
Please also give the version of ``burp`` AND ``Burp-UI``.
Since v0.0.6 you can use the ``-V`` or ``--version`` flag in order to get your
version number.
Notes
-----
Please feel free to report any issues on my `gitlab <https://git.ziirish.me/ziirish/burp-ui/issues>`_
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
----
`Here <https://git.ziirish.me/ziirish/burp-ui/issues?label_name=todo>`_ is a non-exhaustive list of things I'd like to add.
`Here <https://git.ziirish.me/ziirish/burp-ui/issues?label_name=todo>`_ is a
non-exhaustive list of things I'd like to add.
Also note that in the future, I'd like to write a burp-client GUI.
But I didn't think yet of what to do.
Changelog
---------
* version 0.0.5:
* version `current <https://git.ziirish.me/ziirish/burp-ui/>`_:
- Add `gunicorn support <https://git.ziirish.me/ziirish/burp-ui/commit/836f522f51ba0706ca94b379d93b20c75e71ecb1>`_
- Add `init script for CentOS <https://git.ziirish.me/ziirish/burp-ui/issues/27>`_
- Add `init script for Debian <https://git.ziirish.me/ziirish/burp-ui/issues/29>`_
- Add `autofocus login field on login page <https://git.ziirish.me/ziirish/burp-ui/commit/a559c3c2191991f1065ff15df4cd94757133e67d>`_
- Add `burp-server configuration panel <https://git.ziirish.me/ziirish/burp-ui/issues/13>`_
- Fix issue `#25 <https://git.ziirish.me/ziirish/burp-ui/issues/25>`_
- Fix issue `#26 <https://git.ziirish.me/ziirish/burp-ui/issues/26>`_
- Fix issue `#30 <https://git.ziirish.me/ziirish/burp-ui/issues/30>`_
- Fix issue `#32 <https://git.ziirish.me/ziirish/burp-ui/issues/32>`_
- Fix issue `#33 <https://git.ziirish.me/ziirish/burp-ui/issues/33>`_
- Fix issue `#34 <https://git.ziirish.me/ziirish/burp-ui/issues/34>`_
- Fix issue `#35 <https://git.ziirish.me/ziirish/burp-ui/issues/35>`_
- Fix issue `#39 <https://git.ziirish.me/ziirish/burp-ui/issues/39>`_
- Code cleanup
- Improve unit tests
- Bugfixes
- `Full changelog <https://git.ziirish.me/ziirish/burp-ui/compare/v0.0.5...master>`_
* version `0.0.5 <https://git.ziirish.me/ziirish/burp-ui/commits/v0.0.5>`_:
- Add multi-server support
- fix bugs
- Fix bugs
- `Full changelog <https://git.ziirish.me/ziirish/burp-ui/compare/v0.0.4...v0.0.5>`_
* version 0.0.4:
* version `0.0.4 <https://git.ziirish.me/ziirish/burp-ui/commits/v0.0.4>`_:
- Add the ability to download files directly from the web interface
- `Full changelog <https://git.ziirish.me/ziirish/burp-ui/compare/v0.0.3...v0.0.4>`_
* version 0.0.3:
* version `0.0.3 <https://git.ziirish.me/ziirish/burp-ui/commits/v0.0.3>`_:
- Add authentication
- `Full changelog <https://git.ziirish.me/ziirish/burp-ui/compare/v0.0.2...v0.0.3>`_
* version 0.0.2:
* version `0.0.2 <https://git.ziirish.me/ziirish/burp-ui/commits/v0.0.2>`_:
- Fix bugs
- `Full changelog <https://git.ziirish.me/ziirish/burp-ui/compare/v0.0.1...v0.0.2>`_
* version 0.0.1:
* version `0.0.1 <https://git.ziirish.me/ziirish/burp-ui/commits/v0.0.1>`_:
- Initial release
......@@ -120,7 +248,7 @@ Changelog
Licenses
--------
Burp-UI is released under the BSD 3-clause `License`_.
``Burp-UI`` is released under the BSD 3-clause `License`_.
But this project is built on top of other tools listed here:
......@@ -131,11 +259,17 @@ But this project is built on top of other tools listed here:
- `fancytree <https://github.com/mar10/fancytree>`_ (`MIT <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/fancytree/MIT-LICENSE.txt>`__)
- `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>`_
- `bootswatch <http://bootswatch.com/>`_ theme ``Slate`` (`MIT <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/bootstrap/bootswatch.LICENSE>`__)
- `angular-bootstrap-switch <https://github.com/frapontillo/angular-bootstrap-switch>`_ (`Apache <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/angular-bootstrap-switch/LICENSE>`__)
- `angular.js <https://angularjs.org/>`_ (`MIT <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/angularjs/LICENSE>`__)
- `angular-ui-select <https://github.com/angular-ui/ui-select>`_ (`MIT <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/angular-ui-select/LICENSE>`__)
- `AngularStrap <http://mgcrea.github.io/angular-strap/>`_ (`MIT <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/angular-strap/LICENSE.md>`__)
- `lodash <https://github.com/lodash/lodash>`_ (`MIT <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/lodash/LICENSE.txt>`__)
- Home-made `favicon <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/images/favicon.ico>`_ based on pictures from `simpsoncrazy <http://www.simpsoncrazy.com/pictures/homer>`_
Also note that this project is made with the Awesome `Flask`_ micro-framework.
Thanks
------
......
......@@ -2,42 +2,30 @@
# -*- coding: utf8 -*-
import sys
import os
import os.path
from optparse import OptionParser
sys.path.append('{0}/..'.format(os.path.join(os.path.dirname(os.path.realpath(__file__)))))
from burpui import app, bui
from burpui import bui, init, __title__, __version__
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')
parser.add_option('-v', '--verbose', dest='log', help='Verbose output.', action='store_true')
parser.add_option('-d', '--debug', dest='log', help='Verbose output (alias).', action='store_true') #alias for -v
parser.add_option('-V', '--version', dest='version', help='Print version and exit.', action='store_true')
parser.add_option('-c', '--config', dest='config', help='Configuration file.', metavar='CONFIG')
(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):
conf = options.config
app.config['CFG'] = conf
else:
raise IOError('File not found: \'{0}\''.format(options.config))
else:
conf_files = ['/etc/burp/burpui.cfg', os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'share', 'burpui', 'etc', 'burpui.cfg')]
for p in conf_files:
app.logger.debug('Trying file \'%s\'', p)
if os.path.isfile(p):
app.config['CFG'] = p
app.logger.debug('Using file \'%s\'', p)
break
bui.setup(app.config['CFG'])
if options.version:
print '{}: v{}'.format(__title__, __version__)
sys.exit(0)
init(options.config, d, False)
bui.run(d)
......@@ -3,10 +3,15 @@
Burp-UI is a web-ui for burp backup written in python with Flask and
jQuery/Bootstrap
"""
__title__ = 'burp-ui'
__author__ = 'Benjamin SANS (Ziirish)'
__license__ = 'BSD 3-clause'
__title__ = 'burp-ui'
__author__ = 'Benjamin SANS (Ziirish)'
__author_email__ = 'ziirish+burpui@ziirish.info'
__url__ = 'https://git.ziirish.me/ziirish/burp-ui'
__description__ = 'Burp-UI is a web-ui for burp backup written in python with Flask and jQuery/Bootstrap'
__license__ = 'BSD 3-clause'
__version__ = '0.0.6'
import os
from flask import Flask
from flask.ext.login import LoginManager
......@@ -31,3 +36,30 @@ login_manager.login_message_category = 'info'
# Then we load our routes
import burpui.routes
def init(conf=None, debug=False, gunicorn=True):
app.config['DEBUG'] = debug
if debug:
app.config['TESTING'] = True
if conf:
if os.path.isfile(conf):
app.config['CFG'] = conf
else:
raise IOError('File not found: \'{0}\''.format(conf))
else:
conf_files = ['/etc/burp/burpui.cfg', os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', '..', 'share', 'burpui', 'etc', 'burpui.cfg')]
for p in conf_files:
app.logger.debug('Trying file \'%s\'', p)
if os.path.isfile(p):
app.config['CFG'] = p
app.logger.debug('Using file \'%s\'', p)
break
bui.setup(app.config['CFG'])
if gunicorn:
from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)
return app
# -*- coding: utf8 -*-
import os
import sys
import struct
import select
import json
import time
import ConfigParser
import SocketServer
from threading import Thread
g_port = 10000
g_port = '10000'
g_bind = '::'
g_ssl = False
g_version = 1
g_ssl = 'False'
g_version = '1'
g_sslcert = ''
g_sslkey = ''
g_password = 'password'
......@@ -57,14 +57,17 @@ class BUIAgent:
self.methods = {
'status': self.backend.status,
'parse_backup_log': self.backend.parse_backup_log,
'get_backup_logs': self.backend.get_backup_logs,
'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
'restore_files': self.backend.restore_files,
'read_conf': self.backend.read_conf,
'store_conf': self.backend.store_conf,
'get_parser_attr': self.backend.get_parser_attr
}
self.server = AgentServer((self.bind, self.port), AgentTCPHandler, self)
......@@ -85,6 +88,9 @@ class AgentTCPHandler(SocketServer.BaseRequestHandler):
# self.request is the client connection
self.server.agent.debug('===============>')
try:
r, _, _ = select.select([self.request], [], [], 5)
if not r:
raise Exception ('Socket timed-out')
lengthbuf = self.request.recv(8)
length, = struct.unpack('!Q', lengthbuf)
data = self.recvall(length)
......@@ -92,6 +98,9 @@ class AgentTCPHandler(SocketServer.BaseRequestHandler):
self.server.agent.debug('recv: %s', data)
self.server.agent.debug('####################')
j = json.loads(data)
_, w, _ = select.select([], [self.request], [], 5)
if not w:
raise Exception ('Socket timed-out')
if j['password'] != self.server.agent.password:
self.server.agent.debug('-----> Wrong Password <-----')
self.request.sendall('KO')
......@@ -111,6 +120,9 @@ class AgentTCPHandler(SocketServer.BaseRequestHandler):
self.server.agent.debug('####################')
self.server.agent.debug('result: %s', res)
self.server.agent.debug('####################')
_, w, _ = select.select([], [self.request], [], 5)
if not w:
raise Exception ('Socket timed-out')
if j['func'] == 'restore_files':
size = os.path.getsize(res)
self.request.sendall(struct.pack('!Q', size))
......@@ -120,6 +132,9 @@ class AgentTCPHandler(SocketServer.BaseRequestHandler):
self.server.agent.debug('sending %d Bytes', len(buf))
self.request.sendall(buf)
buf = f.read(1024)
_, w, _ = select.select([], [self.request], [], 5)
if not w:
raise Exception ('Socket timed-out')
else:
self.request.sendall(struct.pack('!Q', len(res)))
self.request.sendall(res)
......
This diff is collapsed.
......@@ -9,7 +9,7 @@ class BUIbackend:
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, agent=None):
def get_backup_logs(self, n, c, forward=False, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def get_counters(self, name=None, agent=None):
......@@ -30,5 +30,20 @@ class BUIbackend:
def get_tree(self, name=None, backup=None, root=None, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def restore_files(self, name=None, backup=None, files=None, strip=None, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def read_conf(self, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def store_conf(self, data, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
def get_parser_attr(self, attr=None, agent=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
class BUIserverException(Exception):
pass
class BUIserverException(Exception):
pass
......@@ -2,6 +2,7 @@
import re
import copy
import socket
import select
import sys
import json
import time
......@@ -48,12 +49,12 @@ class Burp(BUIbackend):
"""
return self.servers[agent].status(query)
def parse_backup_log(self, f, n, c=None, agent=None):
def get_backup_logs(self, n, c, forward=False, 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)
return self.servers[agent].get_backup_logs(n, c, forward)
def get_counters(self, name=None, agent=None):
"""
......@@ -108,8 +109,17 @@ class Burp(BUIbackend):
"""
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)
def restore_files(self, name=None, backup=None, files=None, strip=None, agent=None):
return self.servers[agent].restore_files(name, backup, files, strip)
def read_conf(self, agent=None):
return self.servers[agent].read_conf()
def store_conf(self, data, agent=None):
return self.servers[agent].store_conf(data)
def get_parser_attr(self, attr=None, agent=None):
return self.servers[agent].get_parser_attr(attr)
class NClient(BUIbackend):
......@@ -170,44 +180,34 @@ class NClient(BUIbackend):
self.sock.sendall(struct.pack('!Q', length))
self.sock.sendall(raw)
self.app.logger.debug("Sending: %s", raw)
tmp = self.recvall(2)
r, _, _ = select.select([self.sock], [], [], 5)
if not r:
raise Exception ('Socket timed-out')
tmp = self.sock.recv(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)
r, _, _ = select.select([self.sock], [], [], 5)
if not r:
raise Exception ('Socket timed-out')
lengthbuf = self.sock.recv(8)
length, = struct.unpack('!Q', lengthbuf)
if data['func'] == 'restore_files':
toclose = False
res = (self.sock, length)
else:
res = self.recvall(length)
r, _, _ = select.select([self.sock], [], [], 5)
if not r:
raise Exception ('Socket timed-out')
res = self.sock.recv(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
"""
......@@ -220,12 +220,12 @@ class NClient(BUIbackend):
data = {'func': 'status', 'args': {'query': query}}
return json.loads(self.do_command(data))
def parse_backup_log(self, f, n, c=None, agent=None):
def get_backup_logs(self, n, c, forward=False, 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}}
data = {'func': 'get_backup_logs', 'args': {'n': n, 'c': c, 'forward': forward}}
return json.loads(self.do_command(data))
def get_counters(self, name=None, agent=None):
......@@ -263,7 +263,7 @@ class NClient(BUIbackend):
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
and date) of a given client
"""
data = {'func': 'get_client', 'args': {'name': name}}
return json.loads(self.do_command(data))
......@@ -276,7 +276,18 @@ class NClient(BUIbackend):
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}}
def restore_files(self, name=None, backup=None, files=None, strip=None, agent=None):
data = {'func': 'restore_files', 'args': {'name': name, 'backup': backup, 'files': files, 'strip': strip}}
return self.do_command(data)
def read_conf(self, agent=None):
data = {'func': 'read_conf', 'args': None}
return json.loads(self.do_command(data))
def store_conf(self, data, agent=None):
data = {'func': 'store_conf', 'args': {'data': data}}
return json.loads(self.do_comman(data))
def get_parser_attr(self, attr=None, agent=None):
data = {'func': 'get_parser_attr', 'args': {'attr': attr}}
return json.loads(self.do_command(data))
This diff is collapsed.
# -*- coding: utf8 -*-
class BUIparser:
def __init__(self, app=None, conf=None):
self.app = app
self.conf = conf
def read_server_conf(self):
raise NotImplementedError("Sorry, the current Parser does not implement this method!")
def store_server_conf(self, data):
raise NotImplementedError("Sorry, the current Parser does not implement this method!")
def get_priv_attr(self, key):
raise NotImplementedError("Sorry, the current Parser does not implement this method!")
......@@ -7,6 +7,8 @@ It was found on the Internet...
import math
import string
import sys
import inspect
# code from here: http://code.activestate.com/recipes/578323-human-readable-filememory-sizes-v2/
class human_readable( long ):
......@@ -53,3 +55,38 @@ class human_readable( long ):
return "{0:{1}}".format(t,width) if width != "" else t
def currentframe():
"""Return the frame object for the caller's stack frame."""
try:
raise Exception
except:
return sys.exc_info()[2].tb_frame.f_back
class BUIlogging:
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:
"""
Try to guess where was call the function
"""
cf = currentframe()
(frame, filename, line_number, function_name, lines, index) = inspect.getouterframes(cf)[1]
if cf is not None:
cf = cf.f_back
"""
Ugly hack to reformat the message
"""
ar = list(args)
if isinstance(ar[0], str):
ar[0] = filename+':'+str(cf.f_lineno)+' => '+ar[0]
else:
ar = [filename+':'+str(cf.f_lineno)+' => {0}'.format(ar)]
args = tuple(ar)
logs[level](*args)
# -*- coding: utf8 -*-
import math
import select
from zlib import adler32
from time import gmtime, strftime, time, sleep
from time import gmtime, strftime, time
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
......@@ -18,11 +19,43 @@ def load_user(userid):
return bui.uhandler.user(userid)
return None
@app.route('/settings', methods=['GET', 'POST'])
@app.route('/<server>/settings', methods=['GET', 'POST'])
@login_required
def settings(server=None):
if request.method == 'POST':
noti = bui.cli.store_conf(request.form)
return jsonify(notif=noti)
return render_template('settings.html', settings=True, server=server)
@app.route('/api/server-config')
@app.route('/api/<server>/server-config')
@login_required
def read_conf_srv(server=None):
r = bui.cli.read_conf(server)
return jsonify(results=r,
boolean=bui.cli.get_parser_attr('boolean'),
string=bui.cli.get_parser_attr('string'),
integer=bui.cli.get_parser_attr('integer'),
multi=bui.cli.get_parser_attr('multi'),
server_doc=bui.cli.get_parser_attr('server_doc'),
suggest=bui.cli.get_parser_attr('values_server'),
placeholders=bui.cli.get_parser_attr('placeholders'),
defaults=bui.cli.get_parser_attr('defaults_server'))
"""
Here is the API
The whole API returns JSON-formated data
"""
@app.route('/api/restore/<name>/<int:backup>', methods=['POST'])
@app.route('/api/<server>/restore/<name>/<int:backup>', methods=['POST'])
@login_required
def restore(server=None, name=None, backup=None):
l = request.form.get('list')
s = request.form.get('strip')
resp = None
if not l or not name or not backup:
abort(500)
......@@ -31,11 +64,11 @@ def restore(server=None, name=None, backup=None):
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)
archive = bui.cli.restore_files(name, backup, l, s)
if not archive:
abort(500)
try:
resp = send_file(archive, as_attachment=True, attachment_filename=filename)
resp = send_file(archive, as_attachment=True, attachment_filename=filename, mimetype='application/zip')
resp.set_cookie('fileDownload', 'true')
except Exception, e:
app.logger.error(str(e))
......@@ -43,21 +76,20 @@ def restore(server=None, name=None, backup=None):
else:
socket = None
try:
socket, length = bui.cli.restore_files(name, backup, l, server)
socket, length = bui.cli.restore_files(name, backup, l, s, 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:
while received < l:
buf = b''
r, _, _ = select.select([sock], [], [], 5)
if not r:
raise Exception ('Socket timed-out')
buf += sock.recv(bsize)
if not buf:
tries += 1
sleep(0.1)
continue
received += len(buf)
app.logger.debug('%d/%d', received, l)
......@@ -68,7 +100,7 @@ def restore(server=None, name=None, backup=None):
headers.add('Content-Disposition', 'attachment', filename=filename)
headers['Content-Length'] = length
resp = Response(stream_file(socket, length), mimetype='application/octet-stream',
resp = Response(stream_file(socket, length), mimetype='application/zip',
headers=headers, direct_passthrough=True)
resp.set_cookie('fileDownload', 'true')
resp.set_etag('flask-%s-%s-%s' % (
......@@ -80,13 +112,6 @@ def restore(server=None, name=None, backup=None):
abort(500)
return resp
"""
Here is the API
The whole API returns JSON-formated data
"""
@app.route('/api/running-clients.json')
@app.route('/api/<server>/running-clients.json')
@login_required
......@@ -140,8 +165,9 @@ def render_live_tpl(server=None, name=None):
@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()})
if hasattr(bui.cli, 'servers'):
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')
......@@ -242,11 +268,9 @@ def clients_report_json(server=None):
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']), agent=server)
cl.append( { 'name': c['name'], 'stats': bui.cli.parse_backup_log(f, client[-1]['number'], agent=server) } )
cl.append( { 'name': c['name'], 'stats': bui.cli.get_backup_logs(client[-1]['number'], c['name'], agent=server) } )
for b in client:
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))
ba.append(bui.cli.get_backup_logs(b['number'], c['name'], True, agent=server))
j.append( { 'clients': cl, 'backups': sorted(ba, key=lambda k: k['end']) } )
return jsonify(results=j)
......@@ -267,11 +291,10 @@ def client_stat_json(server=None, name=None, backup=None):
return jsonify(notif=err)
if backup:
try:
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(name, backup), agent=server)
j = bui.cli.get_backup_logs(backup, name, agent=server)
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
j = bui.cli.parse_backup_log(f, backup, agent=server)
else:
try:
cl = bui.cli.get_client(name, agent=server)
......@@ -279,8 +302,7 @@ def client_stat_json(server=None, name=None, backup=None):
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']), agent=server)
j.append(bui.cli.parse_backup_log(f, c['number'], agent=server))
j.append(bui.cli.get_backup_logs(c['number'], name, agent=server))
return jsonify(results=j)
@app.route('/api/client.json/<name>')
......@@ -397,7 +419,7 @@ def client_report(server=None, name=None):
server = request.args.get('server')
l = bui.cli.get_client(name, agent=server)
if len(l) == 1:
return redirect(url_for('backup_report', name=name, backup=l[0]['number']), server=server)
return redirect(url_for('backup_report', name=name, backup=l[0]['number'], server=server))
return render_template('client-report.html', client=True, report=True, cname=name, server=server)
@app.route('/clients-report')
......
......@@ -2,14 +2,14 @@
import ConfigParser
import sys
g_port = 5000
g_port = '5000'
g_bind = '::'
g_refresh = 15
g_ssl = False
g_standalone = True
g_refresh = '15'
g_ssl = 'False'
g_standalone = 'True'
g_sslcert = ''
g_sslkey = ''
g_version = 1
g_version = '1'
g_auth = 'basic'
class BUIServer:
......
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
/**
* angular-bootstrap-switch
* @version v0.3.0 - 2014-06-27
* @author Francesco Pontillo (francescopontillo@gmail.com)
* @link https://github.com/frapontillo/angular-bootstrap-switch
* @license Apache License 2.0
**/
'use strict';
// Source: common/module.js
angular.module('frapontillo.bootstrap-switch', []);
// Source: dist/.temp/directives/bsSwitch.js
angular.module('frapontillo.bootstrap-switch')
.directive('bsSwitch', function ($timeout) {
return {
restrict: 'EA',
require: 'ngModel',
scope: {
switchActive: '@',
switchOnText: '@', // changed name
switchOffText: '@', // changed name
switchOnColor: '@', // changed name
switchOffColor: '@', // changed name
switchAnimate: '@',
switchSize: '@',
switchLabel: '@',
switchIcon: '@', // changed behaviour
switchWrapper: '@' // container class modifier
},
template: function(tElement) {
return ('' + tElement.nodeName).toLowerCase() === 'input' ? undefined : '<input>';
},
replace: true,
link: function link(scope, element, attrs, controller) {
var getTrueValue = function() {
var trueValue = attrs.ngTrueValue;
if (!angular.isString(trueValue)) {
trueValue = true;
}
return trueValue;
};
/**
* Listen to model changes.
*/
var listenToModel = function () {
// When the model changes
controller.$formatters.push(function (newValue) {
if (newValue !== undefined) {
$timeout(function () {
element.bootstrapSwitch('state', (newValue === getTrueValue()), true);
});
}
});
scope.$watch('switchActive', function (newValue) {
var active = newValue === true || newValue === 'true' || !newValue;
element.bootstrapSwitch('disabled', !active);
});
scope.$watch('switchOnText', function (newValue) {
element.bootstrapSwitch('onText', getValueOrUndefined(newValue));
});
scope.$watch('switchOffText', function (newValue) {
element.bootstrapSwitch('offText', getValueOrUndefined(newValue));
});
scope.$watch('switchOnColor', function (newValue) {
attrs.dataOn = newValue;
element.bootstrapSwitch('onColor', getValueOrUndefined(newValue));
});
scope.$watch('switchOffColor', function (newValue) {
attrs.dataOff = newValue;
element.bootstrapSwitch('offColor', getValueOrUndefined(newValue));
});
scope.$watch('switchAnimate', function (newValue) {
element.bootstrapSwitch('animate', scope.$eval(newValue || 'true'));
});
scope.$watch('switchSize', function (newValue) {
element.bootstrapSwitch('size', newValue);
});
scope.$watch('switchLabel', function (newValue) {
element.bootstrapSwitch('labelText', newValue ? newValue : '&nbsp;');
});
scope.$watch('switchIcon', function (newValue) {
if (newValue) {
// build and set the new span
var spanClass = '<span class=\'' + newValue + '\'></span>';
element.bootstrapSwitch('labelText', spanClass);
}
});
scope.$watch('switchWrapper', function(newValue) {
// Make sure that newValue is not empty, otherwise default to null
if (!newValue) {
newValue = null;
}
element.bootstrapSwitch('wrapperClass', newValue);
});
};
/**
* Listen to view changes.
*/
var listenToView = function () {
// When the switch is clicked, set its value into the ngModelController's $viewValue
element.on('switchChange.bootstrapSwitch', function (e, data) {
scope.$apply(function () {
controller.$setViewValue(data);
});
});
};
/**
* Returns the value if it is truthy, or undefined.
*
* @param value The value to check.
* @returns the original value if it is truthy, {@link undefined} otherwise.
*/
var getValueOrUndefined = function(value) {
return (value ? value : undefined);
};
// Wrap in a $timeout to give the ngModelController
// enough time to resolve the $modelValue
$timeout(function () {
var isInitiallyActive = controller.$modelValue === getTrueValue();
// Bootstrap the switch plugin
element.bootstrapSwitch({
state: isInitiallyActive
});
// Listen and respond to model changes
listenToModel();
// Listen and respond to view changes
listenToView();
// Set the initial view value (may differ from the model value)
controller.$setViewValue(isInitiallyActive);
// On destroy, collect ya garbage
scope.$on('$destroy', function () {
element.bootstrapSwitch('destroy');
});
});
}
};
});