Compare commits

...
Sign in to create a new pull request.

77 commits

Author SHA1 Message Date
a4830951b1 Merge pull request #1 from fduraffourg/master
Fix AS path computation for bgpmap & Fix missing import
2015-12-20 02:15:32 +02:00
Florian Duraffourg
4c7d6000be Add missing import 2015-12-17 16:45:08 +01:00
Florian Duraffourg
9cb0073265 Fix on bgpmap path calculation 2015-11-25 14:23:09 +01:00
3adf071c98 Merge branch 'master' of github.com:stv0g/bird-lg 2015-04-04 20:19:48 +02:00
root
ba79e3c575 Merge branch 'master' of github.com:stv0g/bird-lg 2015-04-04 20:19:23 +02:00
b8e7a6b392 made config settings optional without throwing an exception 2015-04-04 20:14:41 +02:00
4c2e99c9ed added ping command 2015-04-04 20:11:02 +02:00
288aca205a capitalize AS in whois result 2015-04-04 19:24:24 +02:00
19f9e2493b improved code formatting 2015-04-04 19:18:30 +02:00
91c9a68b1d removed duplicated function 2015-04-04 18:12:33 +02:00
5b5c234592 bugfix: removed superfluous import 2015-04-04 17:57:57 +02:00
4762588a01 bugfix: do not require BIRD_HAS_FULL_VIEW in config 2015-04-04 17:57:24 +02:00
376a29ea08 Merge remote-tracking branch 'gh/zorun/bird-1.4' 2015-04-04 15:56:03 +02:00
ee4199adf4 Merge remote-tracking branch 'gh/zorun/dn42'
Conflicts:
	lg.cfg
	lg.py
	toolbox.py
2015-04-04 15:52:04 +02:00
2962e4b7ed Merge remote-tracking branch 'gh/dsx/master'
Conflicts:
	.gitignore
	lg.cfg
	lg.py
	lgproxy.cfg
	lgproxy.py
	toolbox.py
2015-04-04 15:48:30 +02:00
e7f3b86287 Merge remote-tracking branch 'gh/zorun/master'
Conflicts:
	.gitignore
	lg.cfg.sample
	lg.py
	lgproxy.cfg.sample
2015-04-04 15:36:23 +02:00
6d74e8d292 moved example configuration and added production config to gitignore 2015-04-04 15:31:31 +02:00
7458ac3c75 added dependencies as Debian packages 2015-04-04 15:28:31 +02:00
Baptiste Jonglez
1c6b39a275 Fix AS name detection in whois 2015-02-11 23:52:32 +01:00
Baptiste Jonglez
6694207ad8 Allow to configure the location of Bird sockets 2015-02-11 23:41:15 +01:00
dsx
85cc385cc2 Two bugfixes -- indentation and actual call to check_features() 2015-01-27 11:55:31 -05:00
dsx
8aadc7c072 Removed unused option 2015-01-27 11:26:55 -05:00
dsx
1ea13f80f4 Added Full view for Bird 2015-01-27 11:22:16 -05:00
dsx
c67f4644ae PEP8 compliance 2015-01-21 12:03:55 -05:00
dsx
8e55d6aac2 full view wip 2015-01-20 12:11:38 -05:00
dsx
1cd21536be added more stuff to gitignore (logs, venv, etc.) 2015-01-20 08:57:15 -05:00
Baptiste Jonglez
51b35242fa Allow to configure the bind address and ports for lgproxy.py 2014-12-15 20:49:00 +01:00
Baptiste Jonglez
dd434d01b4 Cleanup example config files (warning: some values have changed)
Instead of tetaneutral.net's config, use generic examples.  Also, disable
debug by default, and use a consistent log location.
2014-12-15 20:43:00 +01:00
Baptiste Jonglez
cc1f33ee3d Update README 2014-12-15 20:09:15 +01:00
Tassilo Schweyer
9bea48e0e8 use whois instead cymru.com
(missing support for 4byte ASN, etc.)

Conflicts:
	lg.py
	toolbox.py
2014-12-15 20:05:55 +01:00
Tassilo Schweyer
1b04941e9b rename default config to example config
Conflicts:
	lg-proxy.cfg.example
2014-12-15 19:58:13 +01:00
Baptiste Jonglez
e502129656 Fix parsing of 'show protocols' with bird 1.4
The current parsing was broken because of a change in the date format.

Actually, the new method is much simpler, and should resist small syntax
changes in Bird's output.  We don't use an ugly regexp anymore.

Important limitation: parsing will be messed up if the date contains a
space character.  This does not happen with the default date format of
both bird 1.3 and bird 1.4, but since the date format is configurable in
bird, it may happen anyway.
2014-12-15 19:51:27 +01:00
Mehdi Abaakouk
6a7bd7f228 Allow configure asn cache expiration 2014-04-20 17:20:52 +02:00
Mehdi Abaakouk
9822e42e25 Add resolv timeout, store asn in memcache 2014-04-20 17:02:22 +02:00
Mehdi Abaakouk
af2c305049 Merge remote-tracking branch 'zorun/master' 2014-03-18 14:09:36 +01:00
Mehdi Abaakouk
cbdaa9138c Change lg-proxy to lgproxy to easly import it in wsgi part2 2014-02-18 11:57:44 +01:00
Mehdi Abaakouk
63e978f84f Change lg-proxy to lgproxy to easly import it in wsgi 2014-02-18 11:52:43 +01:00
Baptiste Jonglez
717354c503 Fix parsing of 'show protocols' with bird 1.4
The current parsing was broken because of a change in the date format.

Actually, the new method is much simpler, and should resist small syntax
changes in Bird's output.  We don't use an ugly regexp anymore.

Important limitation: parsing will be messed up if the date contains a
space character.  This does not happen with the default date format of
both bird 1.3 and bird 1.4, but since the date format is configurable in
bird, it may happen anyway.
2014-02-12 23:49:21 +01:00
Baptiste Jonglez
ffafef27cd Allow to configure the bind address of bird-lg 2014-02-12 22:09:09 +01:00
Baptiste Jonglez
fe9a7f8fe4 Use traceroute{,6} on BSD instead of traceroute -{4,6} 2014-01-28 17:01:47 +01:00
Baptiste Jonglez
5cb45d5785 Fix commit e75842b0 (typo) 2014-01-28 16:44:32 +01:00
Baptiste Jonglez
e75842b025 Fix traceroute options for FreeBSD, OpenBSD, NetBSD 2014-01-28 16:33:19 +01:00
Baptiste Jonglez
317de87866 Don't hardcode tetaneutral.net in the site title 2014-01-28 16:17:28 +01:00
Baptiste Jonglez
fe4e8caf2f Fix bgpmap (Graphviz does not seem to like empty labels) 2014-01-28 15:49:15 +01:00
Baptiste Jonglez
e557dd651b Catch possible exceptions thrown by the ASN DNS resolver 2014-01-28 15:49:15 +01:00
Baptiste Jonglez
2418d13d07 Add support for configuring the DNS-based ASN → name mapping service. 2014-01-28 15:49:15 +01:00
Baptiste Jonglez
ca7eb2b9ac Add support for a configurable whois server 2014-01-28 15:46:27 +01:00
Baptiste Jonglez
48dc1046ed Catch possible exceptions thrown by the ASN DNS resolver 2014-01-28 15:45:03 +01:00
Baptiste Jonglez
3fae79ed54 Fix bgpmap (Graphviz does not seem to like empty labels) 2014-01-28 15:39:29 +01:00
Baptiste Jonglez
fc6472071b Cleaner solution for making the ASN service configurable 2014-01-28 15:39:29 +01:00
Baptiste Jonglez
ada668055d Add support for configuring the DNS-based ASN → name mapping service.
It's a bit hackish to make it configurable.
2014-01-27 22:33:37 +01:00
Baptiste Jonglez
44aa3d19d4 Add support for a configurable whois server 2014-01-27 22:13:23 +01:00
Mehdi ABAAKOUK
6b7dfbb7a9 Merge pull request #7 from benpro/patch-1
Fix ASCII schema and some sentences.
2013-08-20 01:11:43 -07:00
Benoît.S
3fbdba357d Fix ASCII schema and some sentences. 2013-08-20 09:58:40 +02:00
Mehdi Abaakouk
1d20093048 Add access log 2012-11-27 20:05:54 +01:00
Mehdi Abaakouk
298b1459fa Handle error correctly 2012-10-17 16:33:02 +02:00
Mehdi Abaakouk
6311df95e8 Rewrite the way that user input is handled 2012-10-17 16:17:52 +02:00
Mehdi Abaakouk
5bb5d42d0f Fix index error in last commit 2012-10-17 11:36:36 +02:00
Mehdi Abaakouk
c22e73c7f8 Fix error when sanitized have only 1 argument 2012-10-17 11:32:33 +02:00
Mehdi Abaakouk
8419152668 Fix issue #2 2012-10-16 08:07:30 +02:00
Mehdi Abaakouk
b593b0cfb7 escape some string 2012-10-16 08:01:10 +02:00
Mehdi Abaakouk
f715dcfeaf Enable log 2012-10-16 08:01:10 +02:00
Mehdi Abaakouk
82b1569dd4 use current file name 2012-08-21 11:55:16 +02:00
Mehdi Abaakouk
17d11c0ec4 Add logging system 2012-08-21 11:12:58 +02:00
Mehdi Abaakouk
f30939f764 Detect site path 2012-08-21 09:28:08 +02:00
Mehdi Abaakouk
ea5563fe81 Move debug boolean to configuration file 2012-08-21 09:21:14 +02:00
Mehdi Abaakouk
9ca18eff4a Made summary sortable 2012-08-10 18:36:09 +02:00
Mehdi Abaakouk
730d1dcffd Merge branch 'master' of git://github.com/sileht/bird-lg 2012-07-27 23:12:27 +02:00
Mehdi Abaakouk
725b8d3a3d basic caracter escape for pydot html label 2012-07-27 23:12:11 +02:00
Mehdi ABAAKOUK
892201f764 Merge pull request #1 from job/patch-1
added dependency to python-pydot
2012-07-25 00:08:45 -07:00
Job Snijders
dde6dbabce added dependency 2012-07-21 18:37:11 +03:00
Mehdi Abaakouk
af5faf98c1 fix incorrect var name 2012-07-20 17:52:06 +02:00
Mehdi Abaakouk
cb6bb4dd31 Add color to peer status 2012-07-20 14:00:20 +02:00
Mehdi Abaakouk
3905d7d0ac Remove duplicate label on a edge 2012-07-19 17:28:32 +02:00
Mehdi Abaakouk
63a6f32d5f Use service cymru.com service instead of whois 2012-07-18 23:46:44 +02:00
Mehdi Abaakouk
d148daf4e2 Fix exception when bgpmap can not be generated 2012-07-16 08:09:17 +02:00
Mehdi Abaakouk
1474ff90aa Allow html string in error/warning messages 2012-07-16 08:08:47 +02:00
24 changed files with 13017 additions and 331 deletions

9
.gitignore vendored
View file

@ -1,2 +1,7 @@
*.pyc
*.pyo
*.py[co]
*.egg
*.egg-info
eggs
*.cfg
*.log

View file

@ -1,42 +1,58 @@
BIRD-LG
=======
this is a looking glass for the Internet Routing Daemon "Bird"
This is a looking glass for the Internet Routing Daemon "Bird".
software is splited onto two parts:
- lg-proxy.py:
It must be install and started on all bird node. It act as a proxy to make traceroute and bird query on the node
Also the access restriction to this web service can be done is file "lg-proxy.cfg" (only ip based restriction for now)
Software is split in two parts:
- lgproxy.py:
It must be installed and started on all bird nodes. It act as a proxy to make traceroute and bird query on the node.
Access restriction to this web service can be done in file "lgproxy.cfg" (only IP address based restriction for now).
- lg.py:
This is the frontend, a web based UI that request information to all lg-proxy.py nodes
The domain and the list of all bird node can be done
This is the frontend, a web based UI that request informations to all lgproxy.py nodes.
The domain and the list of all bird nodes can be done.
```
***************
+--> * lg-proxy.py *
+--> * lgproxy.py *
| ***************
|
******** ******************* | ***************
* USER * ----> * webserver/lg.py *--+--> * lg-proxy.py *
* USER * ----> * webserver/lg.py *--+--> * lgproxy.py *
******** ******************* | ***************
|
| ***************
+--> * lg-proxy.py *
+--> * lgproxy.py *
***************
```
Installation
------------
```
apt-get install python-memcache python-flask python-pydot python-dnspython libapache2-mod-wsgi
```
bird-lg depends on :
bird-lg depend on :
- python-flask >= 0.8
- python-dnspython
- python-pydot
Each service can by embeded in any webserver by follow regular python-flask configuration
Each services can be embedded in any webserver by following regular python-flask configuration.
Only tested with bird 1.2.5
You should copy the configuration files (`lgproxy.cfg.example` to `lgproxy.cfg`
and `lg.cfg.example` to `lg.cfg`) and edit them.
source code under GPL 3.0, powered by Flask, jQuery and Bootstrap
Should work with most Bird versions (from 1.2.x, tested up to 1.4.5).
Copyright (c) 2012 Mehdi Abaakouk <sileht@sileht.net>
Source code is under GPL 3.0, powered by Flask, jQuery and Bootstrap.
Copyright © 2012 Mehdi Abaakouk <sileht@sileht.net>

View file

@ -1,4 +0,0 @@
ACCESS_LIST = ["91.224.149.206", "178.33.111.110"]
IPV4_SOURCE="91.224.148.1"
IPV6_SOURCE="2a01:6600:8000::1"

View file

@ -1,84 +0,0 @@
# -*- coding: utf-8 -*-
# vim: ts=4
###
#
# Copyright (c) 2006 Mehdi Abaakouk
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
#
###
import subprocess
from urllib import unquote
from bird import BirdSocket
from flask import Flask, request, abort
app = Flask(__name__)
app.config.from_pyfile('lg-proxy.cfg')
def check_accesslist():
if app.config["ACCESS_LIST"] and request.remote_addr not in app.config["ACCESS_LIST"]:
abort(401)
@app.route("/traceroute")
@app.route("/traceroute6")
def traceroute():
check_accesslist()
src = []
if request.path == '/traceroute6':
o = "-6"
if app.config.get("IPV6_SOURCE",""):
src = [ "-s", app.config.get("IPV6_SOURCE") ]
else:
o = "-4"
if app.config.get("IPV4_SOURCE",""):
src = [ "-s", app.config.get("IPV4_SOURCE") ]
query = request.args.get("q","")
query = unquote(query)
command = [ 'traceroute' , o ] + src + [ '-A', '-q1', '-N32', '-w1', '-m15', query ]
result = subprocess.Popen( command , stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n","<br>")
return result
@app.route("/bird")
@app.route("/bird6")
def bird():
check_accesslist()
if request.path == "/bird": b = BirdSocket(file="/var/run/bird.ctl")
elif request.path == "/bird6": b = BirdSocket(file="/var/run/bird6.ctl")
else: return "No bird socket selected"
query = request.args.get("q","")
query = unquote(query)
status, result = b.cmd(query)
b.close()
# FIXME: use status
return result
if __name__ == "__main__":
app.debug = True
app.run("0.0.0.0")

20
lg.cfg
View file

@ -1,20 +0,0 @@
DOMAIN = "tetaneutral.net"
PROXY = {
"gw": 5000,
"h3": 5000,
}
# Used for bgpmap
ROUTER_IP = {
"gw" : [ "91.224.148.2", "2a01:6600:8000::175" ],
"h3" : [ "91.224.148.3", "2a01:6600:8000::131" ]
}
AS_NUMBER = {
"gw" : "197422",
"h3" : "197422"
}
SESSION_KEY = '\xd77\xf9\xfa\xc2\xb5\xcd\x85)`+H\x9d\xeeW\\%\xbe/\xbaT\x89\xe8\xa7'

35
lg.cfg.example Normal file
View file

@ -0,0 +1,35 @@
# Configuration file for lg.py
# Copy to lg.cfg and edit to suit your needs.
DEBUG = False
LOG_FILE="/var/log/bird-lg/lg.log"
LOG_LEVEL="WARNING"
DOMAIN = "example.com"
# Which IP/port to listen to for client connections.
BIND_IP = "0.0.0.0"
BIND_PORT = 5000
# Which backend routers to connect to (here, router1.example.com:5000 and
# router2.example.com:5000). The routers must run lgproxy.py.
PROXY = {
"router1": 5000,
"router2": 5000,
}
# Used for bgpmap
ROUTER_IP = {
"router1" : [ "192.0.2.1", "2001:db8::1" ],
"router2" : [ "192.0.2.2", "2001:db8::2" ]
}
AS_NUMBER = {
"router1" : "64498",
"router2" : "65538"
}
#WHOIS_SERVER = "whois.foo.bar"
SESSION_KEY = '\xd77\xf9\xfa\xc2\xb5\xcd\x85)`+H\x9d\xeeW\\%\xbe/\xbaT\x89\xe8\xa7'

441
lg.py Executable file → Normal file
View file

@ -20,20 +20,45 @@
#
###
import subprocess
import re
from urllib2 import urlopen
from collections import defaultdict
from logging.handlers import TimedRotatingFileHandler
from urllib import quote, unquote
from urllib2 import urlopen
import json
import logging
import memcache
import random
import re
import subprocess
from toolbox import mask_is_valid, ipv6_is_valid, ipv4_is_valid, resolve, save_cache_pickle, load_cache_pickle
from toolbox import mask_is_valid, ipv6_is_valid, ipv4_is_valid, resolve, resolve_ptr, save_cache_pickle, load_cache_pickle, get_asname_from_whois, unescape
from dns.resolver import NXDOMAIN
from flask import Flask, render_template, jsonify, redirect, session, request, abort, Response, Markup
import pydot
from flask import Flask, render_template, jsonify, redirect, session, request, abort, Response
app = Flask(__name__)
app.config.from_pyfile('lg.cfg')
app.secret_key = app.config["SESSION_KEY"]
app.debug = app.config["DEBUG"]
file_handler = TimedRotatingFileHandler(filename=app.config["LOG_FILE"], when="midnight")
file_handler.setLevel(getattr(logging, app.config["LOG_LEVEL"].upper()))
app.logger.addHandler(file_handler)
memcache_server = app.config.get("MEMCACHE_SERVER", "127.0.0.1:11211")
memcache_expiration = int(app.config.get("MEMCACHE_EXPIRATION", "1296000")) # 15 days by default
mc = memcache.Client([memcache_server])
def get_asn_from_as(n):
asn_zone = app.config.get("ASN_ZONE", "asn.cymru.com")
try:
data = resolve("AS%s.%s" % (n, asn_zone), "TXT").replace("'", "").replace('"', '')
except:
return " " * 5
return [field.strip() for field in data.split("|")]
def add_links(text):
@ -46,16 +71,18 @@ def add_links(text):
ret_text = []
for line in text:
# Some heuristic to create link
if line.strip().startswith("BGP.as_path:") or \
line.strip().startswith("Neighbor AS:"):
ret_text.append(re.sub(r'(\d+)', r'<a href="/whois/\1" class="whois">\1</a>', line))
if line.strip().startswith("BGP.as_path:") or line.strip().startswith("Neighbor AS:"):
ret_text.append(re.sub(r'(\d+)', r'<a href="/whois?q=\1" class="whois">\1</a>', line))
else:
line = re.sub(r'([a-zA-Z0-9\-]*\.([a-zA-Z]{2,3}){1,2})(\s|$)', r'<a href="/whois/\1" class="whois">\1</a>\3', line)
line = re.sub(r'AS(\d+)', r'<a href="/whois/\1" class="whois">AS\1</a>', line)
line = re.sub(r'(\d+\.\d+\.\d+\.\d+)', r'<a href="/whois/\1" class="whois">\1</a>', line)
hosts = "/".join(request.path.split("/")[2:])
line = re.sub(r'([a-zA-Z0-9\-]*\.([a-zA-Z]{2,3}){1,2})(\s|$)', r'<a href="/whois?q=\1" class="whois">\1</a>\3', line)
line = re.sub(r'AS(\d+)', r'<a href="/whois?q=\1" class="whois">AS\1</a>', line)
line = re.sub(r'(\d+\.\d+\.\d+\.\d+)', r'<a href="/whois?q=\1" class="whois">\1</a>', line)
if len(request.path) >= 2:
hosts = "/".join(request.path.split("/")[2:])
else:
hosts = "/"
line = re.sub(r'\[(\w+)\s+((|\d\d\d\d-\d\d-\d\d\s)(|\d\d:)\d\d:\d\d|\w\w\w\d\d)', r'[<a href="/detail/%s?q=\1">\1</a> \2' % hosts, line)
line = re.sub(r'(^|\s+)(([a-f\d]{0,4}:){3,10}[a-f\d]{0,4})', r'\1<a href="/whois/\2" class="whois">\2</a>', line, re.I)
line = re.sub(r'(^|\s+)(([a-f\d]{0,4}:){3,10}[a-f\d]{0,4})', r'\1<a href="/whois?q=\2" class="whois">\2</a>', line, re.I)
ret_text.append(line)
return "\n".join(ret_text)
@ -83,7 +110,10 @@ def set_session(request_type, hosts, proto, request_args):
def whois_command(query):
return subprocess.Popen(['whois', query], stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore')
server = []
if app.config.get("WHOIS_SERVER", ""):
server = ["-h", app.config.get("WHOIS_SERVER")]
return subprocess.Popen(['whois'] + server + [query], stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore')
def bird_command(host, proto, query):
@ -92,9 +122,9 @@ def bird_command(host, proto, query):
def bird_proxy(host, proto, service, query):
"""Retreive data of a service from a running lg-proxy on a remote node
"""Retreive data of a service from a running lgproxy on a remote node
First and second arguments are the node and the port of the running lg-proxy
First and second arguments are the node and the port of the running lgproxy
Third argument is the service, can be "traceroute" or "bird"
Last argument, the query to pass to the service
@ -109,10 +139,12 @@ def bird_proxy(host, proto, service, query):
port = app.config["PROXY"].get(host, "")
if not port or not path:
return False, "Host/Proto not allowed"
if not port:
return False, 'Host "%s" invalid' % host
elif not path:
return False, 'Proto "%s" invalid' % proto
else:
url = "http://%s.%s:%d/%s?q=%s" % (host, app.config["DOMAIN"], port, path, quote(query))
url = 'http://{}:{}/{}?q={}'.format(app.config['ROUTER_IP'][host][0], port, path, quote(query))
try:
f = urlopen(url)
resultat = f.read()
@ -126,18 +158,19 @@ def bird_proxy(host, proto, service, query):
@app.context_processor
def inject_commands():
commands = [
("traceroute", "traceroute ..."),
("summary", "show protocols"),
("detail", "show protocols ... all"),
("prefix", "show route for ..."),
("prefix_detail", "show route for ... all"),
("prefix_bgpmap", "show route for ... (bgpmap)"),
("where", "show route where net ~ [ ... ]"),
("where_detail", "show route where net ~ [ ... ] all"),
("where_bgpmap", "show route where net ~ [ ... ] (bgpmap)"),
("adv", "show route ..."),
("adv_bgpmap", "show route ... (bgpmap)"),
]
("ping", "ping ..."),
("traceroute", "traceroute ..."),
("summary", "show protocols"),
("detail", "show protocols ... all"),
("prefix", "show route for ..."),
("prefix_detail", "show route for ... all"),
("prefix_bgpmap", "show route for ... (bgpmap)"),
("where", "show route where net ~ [ ... ]"),
("where_detail", "show route where net ~ [ ... ] all"),
("where_bgpmap", "show route where net ~ [ ... ] (bgpmap)"),
("adv", "show route ..."),
("adv_bgpmap", "show route ... (bgpmap)"),
]
commands_dict = {}
for id, text in commands:
commands_dict[id] = text
@ -155,27 +188,33 @@ def hello():
def error_page(text):
return render_template('error.html', error=text), 500
return render_template('error.html', errors=[text]), 500
@app.errorhandler(400)
def incorrect_request(e):
return render_template('error.html', warning="The server could not understand the request"), 400
return render_template('error.html', warnings=["The server could not understand the request"]), 400
@app.errorhandler(404)
def page_not_found(e):
return render_template('error.html', warning="The requested URL was not found on the server."), 404
return render_template('error.html', warnings=["The requested URL was not found on the server."]), 404
@app.route("/whois/<query>")
def whois(query):
if not query.strip():
def get_query():
q = unquote(request.args.get('q', '').strip())
return q
@app.route("/whois")
def whois():
query = get_query()
if not query:
abort(400)
try:
asnum = int(query)
query = "as%d" % asnum
query = "AS%d" % asnum
except:
m = re.match(r"[\w\d-]*\.(?P<domain>[\d\w-]+\.[\d\w-]+)$", query)
if m:
@ -186,41 +225,54 @@ def whois(query):
SUMMARY_UNWANTED_PROTOS = ["Kernel", "Static", "Device"]
SUMMARY_RE_MATCH = r"(?P<name>[\w_]+)\s+(?P<proto>\w+)\s+(?P<table>\w+)\s+(?P<state>\w+)\s+(?P<since>((|\d\d\d\d-\d\d-\d\d\s)(|\d\d:)\d\d:\d\d|\w\w\w\d\d))($|\s+(?P<info>.*))"
@app.route("/summary/<hosts>")
@app.route("/summary/<hosts>/<proto>")
def summary(hosts, proto="ipv4"):
set_session("summary", hosts, proto, "")
command = "show protocols"
summary = {}
error = []
errors = []
for host in hosts.split("+"):
ret, res = bird_command(host, proto, command)
res = res.split("\n")
if len(res) > 1:
data = []
for line in res[1:]:
line = line.strip()
if line and (line.split() + [""])[1] not in SUMMARY_UNWANTED_PROTOS:
m = re.match(SUMMARY_RE_MATCH, line)
if m:
data.append(m.groupdict())
else:
app.logger.warning("couldn't parse: %s", line)
summary[host] = data
else:
error.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
if ret is False:
errors.append("%s" % res)
continue
return render_template('summary.html', summary=summary, command=command, error="<br>".join(error))
if len(res) <= 1:
errors.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
continue
data = []
for line in res[1:]:
line = line.strip()
if line and (line.split() + [""])[1] not in SUMMARY_UNWANTED_PROTOS:
split = line.split()
if len(split) >= 5:
props = dict()
props["name"] = split[0]
props["proto"] = split[1]
props["table"] = split[2]
props["state"] = split[3]
props["since"] = split[4]
props["info"] = ' '.join(split[5:]) if len(split) > 5 else ""
data.append(props)
else:
app.logger.warning("couldn't parse: %s", line)
summary[host] = data
return render_template('summary.html', summary=summary, command=command, errors=errors)
@app.route("/detail/<hosts>/<proto>")
def detail(hosts, proto):
name = request.args.get('q', '').strip()
name = get_query()
if not name:
abort(400)
@ -228,21 +280,60 @@ def detail(hosts, proto):
command = "show protocols all %s" % name
detail = {}
error = []
errors = []
for host in hosts.split("+"):
ret, res = bird_command(host, proto, command)
res = res.split("\n")
if len(res) > 1:
detail[host] = {"status": res[1], "description": add_links(res[2:])}
else:
error.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
return render_template('detail.html', detail=detail, command=command, error="<br>".join(error))
if ret is False:
errors.append("%s" % res)
continue
if len(res) <= 1:
errors.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
continue
detail[host] = {"status": res[1], "description": add_links(res[2:])}
return render_template('detail.html', detail=detail, command=command, errors=errors)
@app.route("/ping/<hosts>/<proto>")
def ping(hosts, proto):
q = get_query()
if not q:
abort(400)
set_session("ping", hosts, proto, q)
if proto == "ipv6" and not ipv6_is_valid(q):
try:
q = resolve(q, "AAAA")
except:
return error_page("%s is unresolvable or invalid for %s" % (q, proto))
if proto == "ipv4" and not ipv4_is_valid(q):
try:
q = resolve(q, "A")
except:
return error_page("%s is unresolvable or invalid for %s" % (q, proto))
errors = []
infos = {}
for host in hosts.split("+"):
status, resultat = bird_proxy(host, proto, "ping", q)
if status is False:
errors.append("%s" % resultat)
continue
infos[host] = add_links(resultat)
return render_template('ping.html', infos=infos, errors=errors)
@app.route("/traceroute/<hosts>/<proto>")
def traceroute(hosts, proto):
q = request.args.get('q', '').strip()
q = get_query()
if not q:
abort(400)
@ -259,11 +350,16 @@ def traceroute(hosts, proto):
except:
return error_page("%s is unresolvable or invalid for %s" % (q, proto))
errors = []
infos = {}
for host in hosts.split("+"):
status, resultat = bird_proxy(host, proto, "traceroute", q)
if status is False:
errors.append("%s" % resultat)
continue
infos[host] = add_links(resultat)
return render_template('traceroute.html', infos=infos)
return render_template('traceroute.html', infos=infos, errors=errors)
@app.route("/adv/<hosts>/<proto>")
@ -306,31 +402,21 @@ def show_route_for_bgpmap(hosts, proto):
return show_route("prefix_bgpmap", hosts, proto)
ASNAME_CACHE_FILE = "/tmp/asname_cache.pickle"
ASNAME_CACHE = load_cache_pickle(ASNAME_CACHE_FILE, {})
def get_as_name(_as):
"""return a string that contain the as number following by the as name
It's the use whois database informations
# Warning, the server can be blacklisted from ripe is too many requests are done
"""
if not _as:
return "AS?????"
if not _as.isdigit():
return _as.strip()
if _as not in ASNAME_CACHE:
whois_answer = whois_command("as%s" % _as)
as_name = re.search('(as-name|ASName): (.*)', whois_answer)
if as_name:
ASNAME_CACHE[_as] = as_name.group(2).strip()
else:
ASNAME_CACHE[_as] = _as
save_cache_pickle(ASNAME_CACHE_FILE, ASNAME_CACHE)
if ASNAME_CACHE[_as] == _as:
return "AS%s" % _as
else:
return "AS%s\r%s" % (_as, ASNAME_CACHE[_as])
name = get_asname_from_whois(whois_command('AS' + _as)).replace(" ","\r",1)
return "AS%s | %s" % (_as, name)
def get_as_number_from_protocol_name(host, proto, protocol):
ret, res = bird_command(host, proto, "show protocols all %s" % protocol)
@ -345,20 +431,26 @@ def get_as_number_from_protocol_name(host, proto, protocol):
def show_bgpmap():
"""return a bgp map in a png file, from the json tree in q argument"""
data = request.args.get('q', '').strip()
data = get_query()
if not data:
abort(400)
data = json.loads(unquote(data))
data = json.loads(data)
graph = pydot.Dot('BGPMAP', graph_type='digraph')
nodes = {}
edges = {}
def escape(label):
label = label.replace("&", "&amp;")
label = label.replace(">", "&gt;")
label = label.replace("<", "&lt;")
return label
def add_node(_as, **kwargs):
if _as not in nodes:
kwargs["label"] = '<<TABLE CELLBORDER="0" BORDER="0" CELLPADDING="0" CELLSPACING="0"><TR><TD ALIGN="CENTER">' + kwargs.get("label", get_as_name(_as)).replace("\r","<BR/>") + "</TD></TR></TABLE>>"
kwargs["label"] = '<<TABLE CELLBORDER="0" BORDER="0" CELLPADDING="0" CELLSPACING="0"><TR><TD ALIGN="CENTER">' + escape(kwargs.get("label", get_as_name(_as))).replace("\r", "<BR/>") + "</TD></TR></TABLE>>"
nodes[_as] = pydot.Node(_as, style="filled", fontsize="10", **kwargs)
graph.add_node(nodes[_as])
return nodes[_as]
@ -374,11 +466,19 @@ def show_bgpmap():
edges[edge_tuple] = edge
elif "label" in kwargs and kwargs["label"]:
e = edges[edge_tuple]
e.set_label(e.get_label() + "\r" + kwargs["label"])
label_without_star = kwargs["label"].replace("*", "")
labels = e.get_label().split("\r")
if "%s*" % label_without_star not in labels:
labels = [kwargs["label"]] + [l for l in labels if not l.startswith(label_without_star)]
labels = sorted(labels, cmp=lambda x, y: x.endswith("*") and -1 or 1)
label = escape("\r".join(labels))
e.set_label(label)
return edges[edge_tuple]
for host, asmaps in data.iteritems():
add_node(host, label= "%s\r%s" % (host.upper(), app.config["DOMAIN"].upper()), shape="box", fillcolor="#F5A9A9")
add_node(host, label="%s\r%s" % (host.upper(), app.config["DOMAIN"].upper()), shape="box", fillcolor="#F5A9A9")
as_number = app.config["AS_NUMBER"].get(host, None)
if as_number:
@ -386,9 +486,9 @@ def show_bgpmap():
edge = add_edge(as_number, nodes[host])
edge.set_color("red")
edge.set_style("bold")
#colors = [ "#009e23", "#1a6ec1" , "#d05701", "#6f879f", "#939a0e", "#0e9a93", "#9a0e85", "#56d8e1" ]
# colors = [ "#009e23", "#1a6ec1" , "#d05701", "#6f879f", "#939a0e", "#0e9a93", "#9a0e85", "#56d8e1" ]
previous_as = None
hosts = data.keys()
for host, asmaps in data.iteritems():
first = True
@ -403,18 +503,23 @@ def show_bgpmap():
continue
if not hop:
hop = True
if _as not in hosts:
hop_label = _as
if app.config.get('BIRD_HAS_FULL_VIEW', False):
hop = True
hop_label = ''
continue
elif _as not in hosts:
hop_label = _as
if first:
hop_label = hop_label + "*"
continue
else:
hop_label = ""
add_node(_as, fillcolor=(first and "#F5A9A9" or "white"))
edge = add_edge(nodes[previous_as], nodes[_as] , label=hop_label, fontsize="7")
if hop_label:
edge = add_edge(nodes[previous_as], nodes[_as], label=hop_label, fontsize="7")
else:
edge = add_edge(nodes[previous_as], nodes[_as], fontsize="7")
hop_label = ""
@ -428,10 +533,11 @@ def show_bgpmap():
previous_as = _as
first = False
node = add_node(previous_as)
node.set_shape("box")
if previous_as:
node = add_node(previous_as)
node.set_shape("box")
#return Response("<pre>" + graph.create_dot() + "</pre>")
# return Response("<pre>" + graph.create_dot() + "</pre>")
return Response(graph.create_png(), mimetype='image/png')
@ -441,10 +547,14 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
path = None
paths = []
net_dest = None
re_via = re.compile(r'(.*)via\s+([0-9a-fA-F:\.]+)\s+on.*\[(\w+)\s+')
re_unreachable = re.compile(r'(.*)unreachable\s+\[(\w+)\s+')
for line in text:
line = line.strip()
expr = re.search(r'(.*)via\s+([0-9a-fA-F:\.]+)\s+on.*\[(\w+)\s+', line)
expr = re_via.search(line)
if expr:
if path:
path.append(net_dest)
@ -464,12 +574,26 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
break
else:
# ugly hack for good printing
path = [ peer_protocol_name ]
# path = ["%s\r%s" % (peer_protocol_name, get_as_name(get_as_number_from_protocol_name(host, proto, peer_protocol_name)))]
path = [peer_protocol_name]
# path = ["%s\r%s" % (peer_protocol_name, get_as_name(get_as_number_from_protocol_name(host, proto, peer_protocol_name)))]
expr2 = re_unreachable.search(line)
if expr2:
if path:
path.append(net_dest)
paths.append(path)
path = None
if expr2.group(1).strip():
net_dest = expr2.group(1).strip()
if line.startswith("BGP.as_path:"):
path.extend(line.replace("BGP.as_path:", "").strip().split(" "))
ASes = line.replace("BGP.as_path:", "").strip().split(" ")
if path:
path.extend(ASes)
else:
path = ASes
if path:
path.append(net_dest)
paths.append(path)
@ -477,8 +601,95 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
return paths
def build_as_tree_from_full_view(host, proto, res):
re_chunk_start = re.compile(r'(.*)unreachable\s+\[(.*)\s+.*\s+from\s+(.*)\].*\(.*\)\s\[.*\]')
dest_subnet = None
raw = defaultdict(dict)
for line in res:
line = line.strip()
expr = re_chunk_start.search(line)
if expr:
# Beginning of the BGP reply chunk
if not dest_subnet:
dest_subnet = expr.group(1).strip()
router_tag = expr.group(2).strip()
router_ip = expr.group(3).strip()
try:
router_ip = resolve_ptr(router_ip)
except NXDOMAIN:
# If PTR record can't be found, IP will do too
pass
elif line.startswith('BGP.as_path:'):
# BGP AS path
line = line.replace('BGP.as_path:', '')
line = line.strip()
path = [router_tag, ]
for as_num in line.split(' '):
if as_num:
path.append(as_num)
path_tag = '+'.join(path[1:])
if path_tag not in raw:
raw[path_tag] = list()
raw[path_tag].append(dict(router_tag=router_tag, router_ip=router_ip, path=path))
elif line.startswith('BGP.community:'):
# BGP community
line = line.replace('BGP.community:', '')
line = line.strip()
raw[path_tag][-1]['community'] = line.split(' ')
elif line.startswith('BGP.cluster_list:'):
# BGP cluster size
line = line.replace('BGP.cluster_list:', '')
line = line.strip()
raw[path_tag][-1]['cluster_size'] = len(line.split(' '))
for path_tag in raw:
raw[path_tag] = iter(raw[path_tag])
result = defaultdict(list)
exhausted_tags = set()
existing_paths_num = len(raw)
if len(raw) > app.config.get('MAX_PATHS', 10):
max_paths = existing_paths_num
else:
max_paths = app.config.get('MAX_PATHS', 10)
path_count = 0
while path_count < max_paths:
for path_tag in sorted(raw, key=lambda x: x.count('+')):
if path_tag in exhausted_tags:
continue
try:
path = next(raw[path_tag])
except StopIteration:
exhausted_tags.add(path_tag)
continue
result[path['router_ip']].append(path['path'])
result[path['router_ip']][-1].append(dest_subnet)
path_count += 1
if path_count == max_paths:
break
if path_count == max_paths or len(exhausted_tags) == existing_paths_num:
break
return result
def show_route(request_type, hosts, proto):
expression = unquote(request.args.get('q', '')).strip()
expression = get_query()
if not expression:
abort(400)
@ -498,7 +709,7 @@ def show_route(request_type, hosts, proto):
command = "show route where net ~ [ " + expression + " ]" + all
else:
mask = ""
if len(expression.split("/")) > 1:
if len(expression.split("/")) == 2:
expression, mask = (expression.split("/"))
if not mask and proto == "ipv4":
@ -525,26 +736,32 @@ def show_route(request_type, hosts, proto):
command = "show route for " + expression + all
detail = {}
error = []
errors = []
for host in hosts.split("+"):
ret, res = bird_command(host, proto, command)
res = res.split("\n")
if len(res) > 1:
if bgpmap:
detail[host] = build_as_tree_from_raw_bird_ouput(host, proto, res)
if ret is False:
errors.append("%s" % res)
continue
if len(res) <= 1:
errors.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
continue
if bgpmap:
if app.config.get('BIRD_HAS_FULL_VIEW', False):
detail = build_as_tree_from_full_view(host, proto, res)
else:
detail[host] = add_links(res)
detail[host] = build_as_tree_from_raw_bird_ouput(host, proto, res)
else:
error.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
detail[host] = add_links(res)
if bgpmap:
detail = json.dumps(detail)
return render_template((bgpmap and 'bgpmap.html' or 'route.html'), detail=detail, command=command, expression=expression, error="<br>".join(error))
return render_template((bgpmap and 'bgpmap.html' or 'route.html'), detail=detail, command=command, expression=expression, errors=errors)
app.secret_key = app.config["SESSION_KEY"]
app.debug = True
if __name__ == "__main__":
app.run("0.0.0.0")
app.run(app.config.get("BIND_IP", "0.0.0.0"), app.config.get("BIND_PORT", 5000))

View file

@ -1,5 +1,8 @@
import sys
sys.path.insert(0,"/var/www/lg2.tetaneutral.net/")
import os
sitepath = os.path.realpath(os.path.dirname(__file__))
sys.path.insert(0, sitepath)
from lg import app as application

22
lgproxy.cfg.example Normal file
View file

@ -0,0 +1,22 @@
# Configuration file for lgproxy.py
# Copy to lgproxy.cfg and edit to suit your needs.
DEBUG = False
LOG_FILE = "/var/log/bird-lg/lgproxy.log"
LOG_LEVEL = "WARNING"
# Which IP/port to listen to for the frontend
BIND_IP = "0.0.0.0"
BIND_PORT = 5000
# Who can connect to the proxy to launch Bird commands
ACCESS_LIST = ["192.0.2.42", "2001:db8::42"]
# Location of Bird control sockets, example for Debian > wheezy
#BIRD_SOCKET = "/run/bird/bird.ctl"
#BIRD6_SOCKET = "/run/bird/bird6.ctl"
# Source IP for traceroute/traceroute6
IPV4_SOURCE = "192.0.2.1"
IPV6_SOURCE = "2001:db8::1"

158
lgproxy.py Normal file
View file

@ -0,0 +1,158 @@
# -*- coding: utf-8 -*-
# vim: ts=4
###
#
# Copyright (c) 2006 Mehdi Abaakouk
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
#
###
import sys
import logging
from logging.handlers import TimedRotatingFileHandler
from logging import FileHandler
import subprocess
from urllib import unquote
from bird import BirdSocket
from flask import Flask, request, abort
app = Flask(__name__)
app.debug = app.config["DEBUG"]
app.config.from_pyfile('lgproxy.cfg')
file_handler = TimedRotatingFileHandler(filename=app.config["LOG_FILE"], when="midnight")
app.logger.setLevel(getattr(logging, app.config["LOG_LEVEL"].upper()))
app.logger.addHandler(file_handler)
@app.before_request
def access_log_before(*args, **kwargs):
app.logger.info("[%s] request %s, %s", request.remote_addr, request.url, "|".join(["%s:%s" % (k, v) for k, v in request.headers.items()]))
@app.after_request
def access_log_after(response, *args, **kwargs):
app.logger.info("[%s] reponse %s, %s", request.remote_addr, request.url, response.status_code)
return response
def check_accesslist():
acl = app.config["ACCESS_LIST"]
if acl and request.remote_addr not in acl:
app.logger.warning("Remote address not in ACCESS_LIST: %s", request.remote_addr)
abort(401)
def check_features():
features = app.config.get('FEATURES', [])
if features and request.endpoint not in features:
app.logger.warning("Requested endpoint not in FEATURES: %s", request.endpoint)
abort(401)
@app.route("/ping")
@app.route("/ping6")
def ping():
check_accesslist()
check_features()
src = []
if request.path == '/ping':
ping = 'ping'
if app.config.get("IPV4_SOURCE", ""):
src = ["-I", app.config.get("IPV4_SOURCE")]
else:
ping = 'ping6'
if app.config.get("IPV6_SOURCE", ""):
src = ["-I", app.config.get("IPV6_SOURCE")]
query = request.args.get("q", "")
query = unquote(query)
options = ['-c4', '-i1', '-w5']
command = [ping] + src + options + [query]
result = subprocess.Popen(command, stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n", "<br>")
return result
@app.route("/traceroute")
@app.route("/traceroute6")
def traceroute():
check_accesslist()
check_features()
if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd') or sys.platform.startswith('openbsd'):
traceroute4 = ['traceroute']
traceroute6 = ['traceroute6']
else: # For Linux
traceroute4 = ['traceroute', '-4']
traceroute6 = ['traceroute', '-6']
src = []
if request.path == '/traceroute6':
traceroute = traceroute6
if app.config.get("IPV6_SOURCE", ""):
src = ["-s", app.config.get("IPV6_SOURCE")]
else:
traceroute = traceroute4
if app.config.get("IPV4_SOURCE", ""):
src = ["-s", app.config.get("IPV4_SOURCE")]
query = request.args.get("q", "")
query = unquote(query)
if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd'):
options = ['-a', '-q1', '-w1', '-m15']
elif sys.platform.startswith('openbsd'):
options = ['-A', '-q1', '-w1', '-m15']
else: # For Linux
options = ['-A', '-q1', '-N32', '-w1', '-m15']
command = traceroute + src + options + [query]
result = subprocess.Popen(command, stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n", "<br>")
return result
@app.route("/bird")
@app.route("/bird6")
def bird():
check_accesslist()
check_features()
if request.path == "/bird":
b = BirdSocket(file=app.config.get('BIRD_SOCKET'))
elif request.path == "/bird6":
b = BirdSocket(file=app.config.get('BIRD6_SOCKET'))
else:
return "No bird socket selected"
query = request.args.get("q", "")
query = unquote(query)
status, result = b.cmd(query)
b.close()
# FIXME: use status
return result
if __name__ == "__main__":
app.logger.info("lgproxy start")
app.run(app.config.get("BIND_IP", "0.0.0.0"), app.config.get("BIND_PORT", 5000))

8
lgproxy.wsgi Normal file
View file

@ -0,0 +1,8 @@
import sys
import os
sitepath = os.path.realpath(os.path.dirname(__file__))
sys.path.insert(0, sitepath)
from lgproxy import app as application

View file

@ -0,0 +1,47 @@
div.dataTables_length label {
float: left;
text-align: left;
}
div.dataTables_length select {
width: 75px;
}
div.dataTables_filter label {
float: right;
}
div.dataTables_info {
padding-top: 8px;
}
div.dataTables_paginate {
float: right;
margin: 0;
}
table.table {
clear: both;
margin-bottom: 6px !important;
}
table.table thead .sorting,
table.table thead .sorting_asc,
table.table thead .sorting_desc,
table.table thead .sorting_asc_disabled,
table.table thead .sorting_desc_disabled {
cursor: pointer;
*cursor: hand;
}
table.table thead .sorting { background: url('../img/sort_both.png') no-repeat center right; }
table.table thead .sorting_asc { background: url('../img/sort_asc.png') no-repeat center right; }
table.table thead .sorting_desc { background: url('../img/sort_desc.png') no-repeat center right; }
table.table thead .sorting_asc_disabled { background: url('../img/sort_asc_disabled.png') no-repeat center right; }
table.table thead .sorting_desc_disabled { background: url('../img/sort_desc_disabled.png') no-repeat center right; }
table.dataTable th:active {
outline: none;
}

BIN
static/img/sort_asc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
static/img/sort_both.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/img/sort_desc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

96
static/js/DT_bootstrap.js Normal file
View file

@ -0,0 +1,96 @@
/* Default class modification */
$.extend( $.fn.dataTableExt.oStdClasses, {
"sWrapper": "dataTables_wrapper form-inline"
} );
/* API method to get paging information */
$.fn.dataTableExt.oApi.fnPagingInfo = function ( oSettings )
{
return {
"iStart": oSettings._iDisplayStart,
"iEnd": oSettings.fnDisplayEnd(),
"iLength": oSettings._iDisplayLength,
"iTotal": oSettings.fnRecordsTotal(),
"iFilteredTotal": oSettings.fnRecordsDisplay(),
"iPage": Math.ceil( oSettings._iDisplayStart / oSettings._iDisplayLength ),
"iTotalPages": Math.ceil( oSettings.fnRecordsDisplay() / oSettings._iDisplayLength )
};
}
/* Bootstrap style pagination control */
$.extend( $.fn.dataTableExt.oPagination, {
"bootstrap": {
"fnInit": function( oSettings, nPaging, fnDraw ) {
var oLang = oSettings.oLanguage.oPaginate;
var fnClickHandler = function ( e ) {
e.preventDefault();
if ( oSettings.oApi._fnPageChange(oSettings, e.data.action) ) {
fnDraw( oSettings );
}
};
$(nPaging).addClass('pagination').append(
'<ul>'+
'<li class="prev disabled"><a href="#">&larr; '+oLang.sPrevious+'</a></li>'+
'<li class="next disabled"><a href="#">'+oLang.sNext+' &rarr; </a></li>'+
'</ul>'
);
var els = $('a', nPaging);
$(els[0]).bind( 'click.DT', { action: "previous" }, fnClickHandler );
$(els[1]).bind( 'click.DT', { action: "next" }, fnClickHandler );
},
"fnUpdate": function ( oSettings, fnDraw ) {
var iListLength = 5;
var oPaging = oSettings.oInstance.fnPagingInfo();
var an = oSettings.aanFeatures.p;
var i, j, sClass, iStart, iEnd, iHalf=Math.floor(iListLength/2);
if ( oPaging.iTotalPages < iListLength) {
iStart = 1;
iEnd = oPaging.iTotalPages;
}
else if ( oPaging.iPage <= iHalf ) {
iStart = 1;
iEnd = iListLength;
} else if ( oPaging.iPage >= (oPaging.iTotalPages-iHalf) ) {
iStart = oPaging.iTotalPages - iListLength + 1;
iEnd = oPaging.iTotalPages;
} else {
iStart = oPaging.iPage - iHalf + 1;
iEnd = iStart + iListLength - 1;
}
for ( i=0, iLen=an.length ; i<iLen ; i++ ) {
// Remove the middle elements
$('li:gt(0)', an[i]).filter(':not(:last)').remove();
// Add the new list items and their event handlers
for ( j=iStart ; j<=iEnd ; j++ ) {
sClass = (j==oPaging.iPage+1) ? 'class="active"' : '';
$('<li '+sClass+'><a href="#">'+j+'</a></li>')
.insertBefore( $('li:last', an[i])[0] )
.bind('click', function (e) {
e.preventDefault();
oSettings._iDisplayStart = (parseInt($('a', this).text(),10)-1) * oPaging.iLength;
fnDraw( oSettings );
} );
}
// Add / remove disabled classes from the static elements
if ( oPaging.iPage === 0 ) {
$('li:first', an[i]).addClass('disabled');
} else {
$('li:first', an[i]).removeClass('disabled');
}
if ( oPaging.iPage === oPaging.iTotalPages-1 || oPaging.iTotalPages === 0 ) {
$('li:last', an[i]).addClass('disabled');
} else {
$('li:last', an[i]).removeClass('disabled');
}
}
}
}
} );

12092
static/js/jquery.dataTables.js vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,99 +1,112 @@
$(window).unload(function() {
$(".progress").show()
});
$(window).unload(function(){
$(".progress").show()
});
function change_url(loc){
$(".progress").show(0, function(){
function change_url(loc) {
$(".progress").show(0, function() {
document.location = loc;
});
}
function reload(){
function reload() {
loc = "/" + request_type + "/" + hosts + "/" + proto;
if (request_type != "summary" ){
if( request_args != undefined && request_args != ""){
loc = loc + "?q=" + request_args;
if (request_type != "summary") {
if(request_args != undefined && request_args != "") {
loc = loc + "?q=" + escape(request_args);
change_url(loc)
}
} else {
change_url(loc)
}
}
function update_view(){
function update_view() {
if (request_type == "summary")
$(".navbar-search").hide();
else
$(".navbar-search").show();
$(".navbar li").removeClass('active');
$(".proto a#"+proto).parent().addClass('active');
$(".hosts a[id='"+hosts+"']").parent().addClass('active');
$(".request_type a#"+request_type).parent().addClass('active');
command = $(".request_type a#"+request_type).text().split("...");
$(".request_type a:first").html(command[0] + '<b class="caret"></b>');
if (command[1] != undefined ) {
if (command[1] != undefined)
$(".navbar li:last").html("&nbsp;&nbsp;"+command[1]);
} else {
else
$(".navbar li:last").html("");
}
request_args = $(".request_args").val();
$(".request_args").focus();
$(".request_args").select();
}
$(function(){
$(".history a").click(function (event){
event.preventDefault();
change_url(this.href)
});
$(".modal .modal-footer .btn").click(function(){
$(".modal").modal('hide');
});
$("a.whois").click(function (event){
event.preventDefault();
link = $(this).attr('href');
$.getJSON(link, function(data) {
$(".modal h3").html(data.title);
$(".modal .modal-body > p").html(data.output);
$(".modal").modal('show');
});
});
$(".history a").click(function (){
$(".history li").removeClass("active")
$(this).parent().addClass("active")
$(function() {
$(".history a").click(function (event) {
event.preventDefault();
change_url(this.href)
});
$(".modal .modal-footer .btn").click(function() {
$(".modal").modal('hide');
});
$("a.whois").click(function (event) {
event.preventDefault();
link = $(this).attr('href');
$.getJSON(link, function(data) {
$(".modal h3").html(data.title);
$(".modal .modal-body > p").html(data.output);
$(".modal").modal('show');
});
});
$(".history a").click(function () {
$(".history li").removeClass("active")
$(this).parent().addClass("active")
});
$(".hosts a").click(function(){
hosts = $(this).attr('id');
update_view();
reload();
});
$(".proto a").click(function(){
proto = $(this).attr('id');
update_view();
reload();
});
$(".request_type ul a").click(function(){
if ( request_type.split("_")[0] != $(this).attr('id').split("_")[0] ){
request_args = ""
$(".request_args").val("");
}
request_type = $(this).attr('id');
update_view();
reload();
});
$("form").submit(function(){
update_view();
reload();
});
$('.request_args').val(request_args);
$(".hosts a").click(function() {
hosts = $(this).attr('id');
update_view();
reload();
});
$(".proto a").click(function() {
proto = $(this).attr('id');
update_view();
reload();
});
$(".request_type ul a").click(function() {
if (request_type.split("_")[0] != $(this).attr('id').split("_")[0]) {
request_args = ""
$(".request_args").val("");
}
request_type = $(this).attr('id');
update_view();
reload();
});
$("form").submit(function() {
update_view();
reload();
});
$('.request_args').val(request_args);
update_view();
t = $('.table-summary')
if (t)
t.dataTable({
"bPaginate": false,
});
});

View file

@ -1,11 +1,12 @@
<!doctype html>
<html lang="en">
<title>Tetaneutral.net looking glass</title>
<title>{{config.DOMAIN|capitalize}} looking glass</title>
<head>
<meta charset="UTF-8">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/bootstrap-responsive.min.css') }}">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/docs.css') }}">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/DT_bootstrap.css') }}">
</head>
<body>
<div class="navbar navbar-fixed-top">
@ -63,11 +64,15 @@
<div class="container-fluid">
<div class="row-fluid">
<div class="span8">
{% if warning %}
<div class="alert alert-warning">{{warning}}</div>
{% if warnings %}
<div class="alert alert-warning">
{% for warning in warnings %}{{warning}}<br />{% endfor %}
</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{error}}</div>
{% if errors %}
<div class="alert alert-error">
{% for error in errors %}{{error}}<br />{% endfor %}
</div>
{% endif %}
{% block body %}{% endblock %}
@ -111,10 +116,11 @@
</div>
<script type="text/javascript" src="{{url_for('static', filename='js/jquery.js') }}"></script>
<script type="text/javascript" src="{{url_for('static', filename='js/bootstrap.min.js') }}"></script>
<script type="text/javascript" src="{{url_for('static', filename='js/jquery-impromptu.3.2.min.js') }}"></script>
<script type="text/javascript" src="{{url_for('static', filename='js/jquery.dataTables.js') }}"></script>
<script type="text/javascript" src="{{url_for('static', filename='js/DT_bootstrap.js') }}"></script>
<script type="text/javascript">
request_type = "{{session.request_type}}";
request_args = "{{session.request_args}}";
request_args = "{{session.request_args|safe}}";
hosts = "{{session.hosts}}";
proto = "{{session.proto}}";
history_query = {{session.history|tojson|safe}};

10
templates/ping.html Normal file
View file

@ -0,0 +1,10 @@
{% extends "layout.html" %}
{% block body %}
{% for host in infos %}
<h3 id="ping_cmd_{{host}}">{{host}}/{{session.proto}}: ping {{session.request_args}}</h3><br />
{% if infos[host]|trim %}
<pre>{{infos[host]|trim|safe}}</pre>
{% endif %}
<br />
{% endfor %}
{% endblock %}

View file

@ -1,14 +1,21 @@
{% extends "layout.html" %}
{% block body %}
{% for host in summary %}
<h3>{{host}}: {{command}}</h3><br />
<table class="table table-striped table-bordered table-condensed">
<h3 style="float:left">{{host}}: {{command}}</h3>
<table class="table table-striped table-bordered table-condensed table-summary">
<thead>
<tr><th>Name</th><th>protocol</th><th>table</th><th>state</th><th>since</th><th>info</th></tr>
</thead>
<tbody>
{% for row in summary[host] %}
<tr class="{{ loop.cycle('odd', 'even') }}"><td><a href="/detail/{{host}}/{{session.proto}}?q={{row.name}}">{{row.name}}</a></td><td>{{row.proto}}</td><td>{{row.table}}</td><td>{{row.state}}</td><td>{{row.since}}</td><td>{{row.info}}</td></tr>
<tr class="{{ loop.cycle('odd', 'even') }}">
<td><a href="/detail/{{host}}/{{session.proto}}?q={{row.name}}">{{row.name}}</a></td>
<td>{{row.proto}}</td>
<td>{{row.table}}</td>
<td><span class="label label-{% if row.state == "up" %}success{% elif row.state == "down" %}default{% else %}important{% endif %}">{{row.state}}</span></td>
<td>{{row.since}}</td>
<td>{{row.info}}</td>
</tr>
{% else %}
<tr><td>{{summary[host].error}}</td><td></td><td></td><td></td><td></td><td></td></tr>
{% endfor %}

View file

@ -19,22 +19,51 @@
#
###
from dns import resolver
from dns import resolver, reversename
import socket
import pickle
import xml.parsers.expat
import re
from flask import Flask
resolv = resolver.Resolver()
resolv.timeout = 0.5
resolv.lifetime = 1
app = Flask(__name__)
app.config.from_pyfile('lg.cfg')
def resolve(n, q):
return str(resolver.query(n,q)[0])
return str(resolv.query(n, q)[0])
def resolve_ptr(ip):
ptr = str(resolve(reversename.from_address(ip), 'PTR')).lower()
ptr = ptr.replace(app.config.get('ROUTER_NAME_REMOVE', ''), '')
return ptr
asname_regex = re.compile("(ASName|as-name):\s+(?P<name>\S+)")
def get_asname_from_whois(data):
r = asname_regex.search(data)
if not r:
return 'UNKNOWN-AS'
return r.groupdict()['name']
def mask_is_valid(n):
if not n:
return True
try:
mask = int(n)
return ( mask >= 1 and mask <= 128)
except:
return False
if not n:
return True
try:
mask = int(n)
return (mask >= 1 and mask <= 128)
except:
return False
def ipv4_is_valid(n):
try:
@ -43,6 +72,7 @@ def ipv4_is_valid(n):
except socket.error:
return False
def ipv6_is_valid(n):
try:
socket.inet_pton(socket.AF_INET6, n)
@ -50,20 +80,49 @@ def ipv6_is_valid(n):
except socket.error:
return False
def save_cache_pickle(filename, data):
output = open(filename, 'wb')
pickle.dump(data, output)
output.close()
output = open(filename, 'wb')
pickle.dump(data, output)
output.close()
def load_cache_pickle(filename, default = None):
try:
pkl_file = open(filename, 'rb')
except IOError:
return default
try:
data = pickle.load(pkl_file)
except:
data = default
pkl_file.close()
return data
def load_cache_pickle(filename, default=None):
try:
pkl_file = open(filename, 'rb')
except IOError:
return default
try:
data = pickle.load(pkl_file)
except:
data = default
pkl_file.close()
return data
def unescape(s):
want_unicode = False
if isinstance(s, unicode):
s = s.encode("utf-8")
want_unicode = True
# the rest of this assumes that `s` is UTF-8
list = []
# create and initialize a parser object
p = xml.parsers.expat.ParserCreate("utf-8")
p.buffer_text = True
p.returns_unicode = want_unicode
p.CharacterDataHandler = list.append
# parse the data wrapped in a dummy element
# (needed so the "document" is well-formed)
p.Parse("<e>", 0)
p.Parse(s, 0)
p.Parse("</e>", 1)
# join the extracted strings and return
es = ""
if want_unicode:
es = u""
return es.join(list)