├── .gitignore ├── templates ├── error.html ├── index.html ├── detail.html ├── traceroute.html ├── bgpmap.html ├── route.html ├── summary.html └── layout.html ├── static ├── img │ ├── sort_asc.png │ ├── sort_both.png │ ├── sort_desc.png │ ├── sort_asc_disabled.png │ ├── sort_desc_disabled.png │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png ├── css │ ├── error.txt │ ├── DT_bootstrap.css │ ├── bootstrap-responsive.min.css │ ├── bootstrap-responsive.css │ └── docs.css └── js │ ├── lg.js │ ├── DT_bootstrap.js │ ├── bootstrap.min.js │ └── bootstrap.js ├── lg.wsgi ├── lgproxy.wsgi ├── init ├── README.md ├── bird-lg-proxy.service └── bird-lg-webservice.service ├── COPYING ├── lgproxy.cfg.example ├── lg.cfg.example ├── toolbox.py ├── README.mkd ├── lgproxy.py ├── bird.py ├── lg.py └── gpl-3.0.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | lg.cfg 4 | lgproxy.cfg 5 | -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {% endblock %} 4 | -------------------------------------------------------------------------------- /static/img/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sileht/bird-lg/HEAD/static/img/sort_asc.png -------------------------------------------------------------------------------- /static/img/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sileht/bird-lg/HEAD/static/img/sort_both.png -------------------------------------------------------------------------------- /static/img/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sileht/bird-lg/HEAD/static/img/sort_desc.png -------------------------------------------------------------------------------- /static/img/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sileht/bird-lg/HEAD/static/img/sort_asc_disabled.png -------------------------------------------------------------------------------- /static/img/sort_desc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sileht/bird-lg/HEAD/static/img/sort_desc_disabled.png -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {{ output|safe }} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sileht/bird-lg/HEAD/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sileht/bird-lg/HEAD/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /lg.wsgi: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import os 4 | 5 | sitepath = os.path.realpath(os.path.dirname(__file__)) 6 | sys.path.insert(0, sitepath) 7 | 8 | from lg import app as application 9 | -------------------------------------------------------------------------------- /lgproxy.wsgi: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import os 4 | 5 | sitepath = os.path.realpath(os.path.dirname(__file__)) 6 | sys.path.insert(0, sitepath) 7 | 8 | from lgproxy import app as application 9 | -------------------------------------------------------------------------------- /templates/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {% for host in detail %} 4 |

{{host}}/{{session.proto}}: {{command}}

5 | {{ detail[host].status }}

6 |
 7 | {{ detail[host].description|trim|safe }}
 8 | 
9 |
10 | {% endfor %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /static/css/error.txt: -------------------------------------------------------------------------------- 1 | A less error occured trying to build your bundle. Please paste the error below in an issue for us at http://github.com/twitter/bootsrap! thanks! 2 | 3 | 4 | {"type":"Parse","message":"Syntax Error on line 102","index":2731,"filename":"bootstrap.css","line":102,"column":0,"extract":["","",""]} -------------------------------------------------------------------------------- /templates/traceroute.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {% for host in infos %} 4 |

{{host}}/{{session.proto}}: traceroute {{session.request_args}}


5 | {% if infos[host]|trim %} 6 |
{{infos[host]|trim|safe}}
7 | {% endif %} 8 |
9 | {% endfor %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /init/README.md: -------------------------------------------------------------------------------- 1 | # bird-lg init 2 | 3 | Systemd unit files for the bird-lg webservice, and for the proxy service running on routers. 4 | 5 | You need to adapt the exact command used to start the service (`ExecStart`) and the `User` 6 | under which it should run. Don't run the services as root! 7 | 8 | ## Installation 9 | 10 | Copy the init file under `/etc/systemd/system/` and run: 11 | 12 | systemctl daemon-reload 13 | systemctl start bird-lg-proxy 14 | systemctl enable bird-lg-proxy 15 | 16 | ## Credits 17 | 18 | Adapted from 19 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | bird-lg 2 | ======= 3 | 4 | Copyright (c) 2006 Mehdi Abaakouk 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /templates/bgpmap.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | SVG 5 | PNG 6 |
7 |

{{session.hosts}}: {{command}}

8 | {% if session.request_args != expression|replace("/32","")|replace("/128","") %} 9 | DNS: {{session.request_args}} => {{expression|replace("/32","")|replace("/128","")}}
10 | {% endif %}
11 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /templates/route.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {% for host in detail %} 4 |

5 | {{host}}: {{command}} 6 | View the BGP map 7 |

8 | {% if session.request_args != expression|replace("/32","")|replace("/128","") %} 9 | DNS: {{session.request_args}} => {{expression|replace("/32","")|replace("/128","")}}
10 | {% endif %}
11 |
12 | {{ detail[host]|trim|safe }}
13 | 
14 | {% endfor %} 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /lgproxy.cfg.example: -------------------------------------------------------------------------------- 1 | # Configuration file example for lgproxy.py 2 | # Adapt and copy to lgproxy.cfg 3 | 4 | DEBUG=False 5 | 6 | LOG_FILE="/var/log/lg-proxy/lg-proxy.log" 7 | LOG_LEVEL="WARNING" 8 | # Keep log history indefinitely by default. 9 | LOG_NUM_DAYS=0 10 | 11 | BIND_IP = "0.0.0.0" 12 | BIND_PORT = 5000 13 | 14 | # Used to restrict access to lgproxy based on source IP address. 15 | # Empty list = any IP is allowed to run queries. 16 | ACCESS_LIST = ["91.224.149.206", "178.33.111.110", "2a01:6600:8081:ce00::1"] 17 | 18 | # Used to restrict access to lgproxy based on a shared secret (must also be configured in lg.cfg) 19 | # Empty string or unset = no shared secret is required to run queries. 20 | SHARED_SECRET="ThisTokenIsNotSecret" 21 | 22 | # Used as source address when running traceroute (optional) 23 | IPV4_SOURCE="198.51.100.42" 24 | IPV6_SOURCE="2001:db8:42::1" 25 | 26 | BIRD_SOCKET="/var/run/bird/bird.ctl" 27 | BIRD6_SOCKET="/var/run/bird/bird6.ctl" 28 | 29 | -------------------------------------------------------------------------------- /templates/summary.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {% for host in summary %} 4 |

{{host}}: {{command}}

5 | 6 | 7 | 8 | 9 | 10 | {% for row in summary[host] %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% else %} 20 | 21 | {% endfor %} 22 | 23 |
Nameprotocoltablestatesinceinfo
{{row.name}}{{row.proto}}{{row.table}}{{row.state}}{{row.since}}{{row.info}}
{{summary[host].error}}
24 |
25 | {% endfor %} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /init/bird-lg-proxy.service: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2018 Alsace Réseau Neutre 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | # Debian GNU/Linux: store this in /etc/systemd/system/ 18 | 19 | 20 | [Unit] 21 | Description=BIRD Looking-Glass proxy 22 | After=bird.service bird6.service 23 | 24 | [Service] 25 | Type=simple 26 | ExecStart=/usr/local/lookingglass/lgproxy.py 27 | User=lgproxy 28 | Restart=on-failure 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | -------------------------------------------------------------------------------- /init/bird-lg-webservice.service: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2018 Alsace Réseau Neutre 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | # Debian GNU/Linux: store this in /etc/systemd/system/ 18 | 19 | 20 | [Unit] 21 | Description=BIRD Looking-Glass service 22 | After=apache2.service 23 | 24 | [Service] 25 | Type=simple 26 | User=lookingglass 27 | ExecStart=/usr/local/lookingglass/lg.py 28 | Restart=on-failure 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | 33 | -------------------------------------------------------------------------------- /lg.cfg.example: -------------------------------------------------------------------------------- 1 | # Configuration file example for lg.py 2 | # Adapt and copy to lg.cfg 3 | 4 | WEBSITE_TITLE="Bird-LG / Looking Glass" 5 | DEBUG = False 6 | LOG_FILE="/var/log/lg.log" 7 | LOG_LEVEL="WARNING" 8 | # Keep log history indefinitely by default. 9 | LOG_NUM_DAYS=0 10 | 11 | DOMAIN = "tetaneutral.net" 12 | 13 | # Used to optionally restrict access to lgproxy based on a shared secret. 14 | # Empty string or unset = no shared secret is used to run queries on lgproxies. 15 | SHARED_SECRET="ThisTokenIsNotSecret" 16 | 17 | BIND_IP = "0.0.0.0" 18 | BIND_PORT = 5000 19 | 20 | PROXY = { 21 | "gw": 5000, 22 | "h3": 5000, 23 | } 24 | 25 | # Used for bgpmap 26 | ROUTER_IP = { 27 | "gw" : [ "91.224.148.2", "2a01:6600:8000::175" ], 28 | "h3" : [ "91.224.148.3", "2a01:6600:8000::131" ] 29 | } 30 | 31 | AS_NUMBER = { 32 | "gw" : "197422", 33 | "h3" : "197422" 34 | } 35 | 36 | #WHOIS_SERVER = "whois.foo.bar" 37 | 38 | # DNS zone to query for ASN -> name mapping 39 | ASN_ZONE = "asn.cymru.com" 40 | 41 | # Used for secure session storage, change this 42 | SESSION_KEY = '\xd77\xf9\xfa\xc2\xb5\xcd\x85)`+H\x9d\xeeW\\%\xbe/\xbaT\x89\xe8\xa7' 43 | -------------------------------------------------------------------------------- /static/css/DT_bootstrap.css: -------------------------------------------------------------------------------- 1 | 2 | div.dataTables_length label { 3 | float: left; 4 | text-align: left; 5 | } 6 | 7 | div.dataTables_length select { 8 | width: 75px; 9 | } 10 | 11 | div.dataTables_filter label { 12 | float: right; 13 | } 14 | 15 | div.dataTables_info { 16 | padding-top: 8px; 17 | } 18 | 19 | div.dataTables_paginate { 20 | float: right; 21 | margin: 0; 22 | } 23 | 24 | table.table { 25 | clear: both; 26 | margin-bottom: 6px !important; 27 | } 28 | 29 | table.table thead .sorting, 30 | table.table thead .sorting_asc, 31 | table.table thead .sorting_desc, 32 | table.table thead .sorting_asc_disabled, 33 | table.table thead .sorting_desc_disabled { 34 | cursor: pointer; 35 | *cursor: hand; 36 | } 37 | 38 | table.table thead .sorting { background: url('../img/sort_both.png') no-repeat center right; } 39 | table.table thead .sorting_asc { background: url('../img/sort_asc.png') no-repeat center right; } 40 | table.table thead .sorting_desc { background: url('../img/sort_desc.png') no-repeat center right; } 41 | 42 | table.table thead .sorting_asc_disabled { background: url('../img/sort_asc_disabled.png') no-repeat center right; } 43 | table.table thead .sorting_desc_disabled { background: url('../img/sort_desc_disabled.png') no-repeat center right; } 44 | 45 | table.dataTable th:active { 46 | outline: none; 47 | } 48 | -------------------------------------------------------------------------------- /toolbox.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: ts=4 3 | ### 4 | # 5 | # Copyright (c) 2006 Mehdi Abaakouk 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 3 as 9 | # published by the Free Software Foundation 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA 19 | # 20 | ### 21 | 22 | from dns import resolver 23 | import socket 24 | import pickle 25 | import xml.parsers.expat 26 | 27 | dns_cache = resolver.LRUCache(max_size=10000) 28 | resolv = resolver.Resolver() 29 | resolv.timeout = 0.5 30 | resolv.lifetime = 1 31 | resolv.cache = dns_cache 32 | 33 | def resolve(n, q): 34 | return str(resolv.query(n,q)[0]) 35 | 36 | def mask_is_valid(n): 37 | if not n: 38 | return True 39 | try: 40 | mask = int(n) 41 | return ( mask >= 1 and mask <= 128) 42 | except: 43 | return False 44 | 45 | def ipv4_is_valid(n): 46 | try: 47 | socket.inet_pton(socket.AF_INET, n) 48 | return True 49 | except socket.error: 50 | return False 51 | 52 | def ipv6_is_valid(n): 53 | try: 54 | socket.inet_pton(socket.AF_INET6, n) 55 | return True 56 | except socket.error: 57 | return False 58 | 59 | def save_cache_pickle(filename, data): 60 | output = open(filename, 'wb') 61 | pickle.dump(data, output) 62 | output.close() 63 | 64 | def load_cache_pickle(filename, default = None): 65 | try: 66 | pkl_file = open(filename, 'rb') 67 | except IOError: 68 | return default 69 | try: 70 | data = pickle.load(pkl_file) 71 | except: 72 | data = default 73 | pkl_file.close() 74 | return data 75 | 76 | def unescape(s): 77 | want_unicode = False 78 | if isinstance(s, unicode): 79 | s = s.encode("utf-8") 80 | want_unicode = True 81 | 82 | # the rest of this assumes that `s` is UTF-8 83 | list = [] 84 | 85 | # create and initialize a parser object 86 | p = xml.parsers.expat.ParserCreate("utf-8") 87 | p.buffer_text = True 88 | p.returns_unicode = want_unicode 89 | p.CharacterDataHandler = list.append 90 | 91 | # parse the data wrapped in a dummy element 92 | # (needed so the "document" is well-formed) 93 | p.Parse("", 0) 94 | p.Parse(s, 0) 95 | p.Parse("", 1) 96 | 97 | # join the extracted strings and return 98 | es = "" 99 | if want_unicode: 100 | es = u"" 101 | return es.join(list) 102 | -------------------------------------------------------------------------------- /static/js/lg.js: -------------------------------------------------------------------------------- 1 | const noArgReqs = ["summary"]; 2 | 3 | $(window).unload(function(){ 4 | $(".progress").show() 5 | }); 6 | 7 | function change_url(loc){ 8 | $(".progress").show(0, function(){ 9 | document.location = loc; 10 | }); 11 | } 12 | 13 | function reload(){ 14 | loc = "/" + request_type + "/" + hosts + "/" + proto; 15 | if (!noArgReqs.includes(request_type)){ 16 | if( request_args != undefined && request_args != ""){ 17 | loc = loc + "?q=" + encodeURIComponent(request_args); 18 | change_url(loc) 19 | } 20 | } else { 21 | change_url(loc) 22 | } 23 | } 24 | function update_view(){ 25 | if (noArgReqs.includes(request_type)) 26 | $(".navbar-search").hide(); 27 | else 28 | $(".navbar-search").show(); 29 | 30 | $(".navbar li").removeClass('active'); 31 | 32 | $(".proto a#"+proto).parent().addClass('active'); 33 | $(".hosts a[id='"+hosts+"']").parent().addClass('active'); 34 | $(".request_type a#"+request_type).parent().addClass('active'); 35 | 36 | command = $(".request_type a#"+request_type).text().split("..."); 37 | $(".request_type a:first").html(command[0] + ''); 38 | if (command[1] != undefined ) { 39 | $(".navbar li:last").html("  "+command[1]); 40 | } else { 41 | $(".navbar li:last").html(""); 42 | } 43 | 44 | request_args = $(".request_args").val(); 45 | $(".request_args").focus(); 46 | $(".request_args").select(); 47 | } 48 | $(function(){ 49 | $(".history a").click(function (event){ 50 | event.preventDefault(); 51 | change_url(this.href) 52 | }); 53 | $(".modal .modal-footer .btn").click(function(){ 54 | $(".modal").modal('hide'); 55 | }); 56 | $("a.whois").click(function (event){ 57 | event.preventDefault(); 58 | link = $(this).attr('href'); 59 | $.getJSON(link, function(data) { 60 | $(".modal h3").html(data.title); 61 | $(".modal .modal-body > p").css("white-space", "pre-line").text(data.output); 62 | $(".modal").modal('show'); 63 | }); 64 | }); 65 | 66 | $(".history a").click(function (){ 67 | $(".history li").removeClass("active") 68 | $(this).parent().addClass("active") 69 | }); 70 | 71 | 72 | $(".hosts a").click(function(){ 73 | hosts = $(this).attr('id'); 74 | update_view(); 75 | reload(); 76 | }); 77 | $(".proto a").click(function(){ 78 | proto = $(this).attr('id'); 79 | update_view(); 80 | reload(); 81 | }); 82 | $(".request_type ul a").click(function(){ 83 | if ( request_type.split("_")[0] != $(this).attr('id').split("_")[0] ){ 84 | request_args = "" 85 | $(".request_args").val(""); 86 | } 87 | request_type = $(this).attr('id'); 88 | update_view(); 89 | reload(); 90 | }); 91 | $("form").submit(function(){ 92 | update_view(); 93 | reload(); 94 | }); 95 | $('.request_args').val(request_args); 96 | update_view(); 97 | 98 | t = $('.table-summary') 99 | if (t) t.dataTable( { 100 | "bPaginate": false, 101 | } ); 102 | 103 | }); 104 | 105 | 106 | -------------------------------------------------------------------------------- /static/js/DT_bootstrap.js: -------------------------------------------------------------------------------- 1 | /* Default class modification */ 2 | $.extend( $.fn.dataTableExt.oStdClasses, { 3 | "sWrapper": "dataTables_wrapper form-inline" 4 | } ); 5 | 6 | /* API method to get paging information */ 7 | $.fn.dataTableExt.oApi.fnPagingInfo = function ( oSettings ) 8 | { 9 | return { 10 | "iStart": oSettings._iDisplayStart, 11 | "iEnd": oSettings.fnDisplayEnd(), 12 | "iLength": oSettings._iDisplayLength, 13 | "iTotal": oSettings.fnRecordsTotal(), 14 | "iFilteredTotal": oSettings.fnRecordsDisplay(), 15 | "iPage": Math.ceil( oSettings._iDisplayStart / oSettings._iDisplayLength ), 16 | "iTotalPages": Math.ceil( oSettings.fnRecordsDisplay() / oSettings._iDisplayLength ) 17 | }; 18 | } 19 | 20 | /* Bootstrap style pagination control */ 21 | $.extend( $.fn.dataTableExt.oPagination, { 22 | "bootstrap": { 23 | "fnInit": function( oSettings, nPaging, fnDraw ) { 24 | var oLang = oSettings.oLanguage.oPaginate; 25 | var fnClickHandler = function ( e ) { 26 | e.preventDefault(); 27 | if ( oSettings.oApi._fnPageChange(oSettings, e.data.action) ) { 28 | fnDraw( oSettings ); 29 | } 30 | }; 31 | 32 | $(nPaging).addClass('pagination').append( 33 | '' 37 | ); 38 | var els = $('a', nPaging); 39 | $(els[0]).bind( 'click.DT', { action: "previous" }, fnClickHandler ); 40 | $(els[1]).bind( 'click.DT', { action: "next" }, fnClickHandler ); 41 | }, 42 | 43 | "fnUpdate": function ( oSettings, fnDraw ) { 44 | var iListLength = 5; 45 | var oPaging = oSettings.oInstance.fnPagingInfo(); 46 | var an = oSettings.aanFeatures.p; 47 | var i, j, sClass, iStart, iEnd, iHalf=Math.floor(iListLength/2); 48 | 49 | if ( oPaging.iTotalPages < iListLength) { 50 | iStart = 1; 51 | iEnd = oPaging.iTotalPages; 52 | } 53 | else if ( oPaging.iPage <= iHalf ) { 54 | iStart = 1; 55 | iEnd = iListLength; 56 | } else if ( oPaging.iPage >= (oPaging.iTotalPages-iHalf) ) { 57 | iStart = oPaging.iTotalPages - iListLength + 1; 58 | iEnd = oPaging.iTotalPages; 59 | } else { 60 | iStart = oPaging.iPage - iHalf + 1; 61 | iEnd = iStart + iListLength - 1; 62 | } 63 | 64 | for ( i=0, iLen=an.length ; i'+j+'') 72 | .insertBefore( $('li:last', an[i])[0] ) 73 | .bind('click', function (e) { 74 | e.preventDefault(); 75 | oSettings._iDisplayStart = (parseInt($('a', this).text(),10)-1) * oPaging.iLength; 76 | fnDraw( oSettings ); 77 | } ); 78 | } 79 | 80 | // Add / remove disabled classes from the static elements 81 | if ( oPaging.iPage === 0 ) { 82 | $('li:first', an[i]).addClass('disabled'); 83 | } else { 84 | $('li:first', an[i]).removeClass('disabled'); 85 | } 86 | 87 | if ( oPaging.iPage === oPaging.iTotalPages-1 || oPaging.iTotalPages === 0 ) { 88 | $('li:last', an[i]).addClass('disabled'); 89 | } else { 90 | $('li:last', an[i]).removeClass('disabled'); 91 | } 92 | } 93 | } 94 | } 95 | } ); 96 | 97 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | BIRD-LG 2 | ======= 3 | 4 | Overview 5 | -------- 6 | 7 | This is a looking glass for the Internet Routing Daemon "Bird". 8 | 9 | Software is split in two parts: 10 | 11 | - lgproxy.py: 12 | 13 | It must be installed and started on all bird nodes. It act as a proxy to make traceroute and bird query on the node. 14 | Access restriction to this web service can be done in file "lgproxy.cfg". Two access restriction methods can be configured: 15 | based on source IP address or based on a shared secret. Both methods can be used at the same time. 16 | 17 | - lg.py: 18 | 19 | This is the frontend, a web based UI that request informations to all lgproxy.py nodes. 20 | The domain and the list of all bird nodes can be done. 21 | 22 | 23 | ``` 24 | 25 | 26 | *************** 27 | +--> * lgproxy.py * 28 | | *************** 29 | | 30 | ******** ******************* | *************** 31 | * USER * ----> * webserver/lg.py *--+--> * lgproxy.py * 32 | ******** ******************* | *************** 33 | | 34 | | *************** 35 | +--> * lgproxy.py * 36 | *************** 37 | ``` 38 | 39 | 40 | Installation 41 | ------------ 42 | 43 | The web service (lg.py) depends on: 44 | 45 | - python-flask >= 0.8 46 | - python-dnspython 47 | - python-pydot 48 | - graphviz 49 | - whois 50 | 51 | The proxy running on routers (lgproxy.py) depends on: 52 | 53 | - python-flask >= 0.8 54 | - traceroute 55 | - ping 56 | 57 | Each service can be embedded in any webserver by following regular python-flask configuration. 58 | It is also possible to run the services directly with python for developping / testing: 59 | 60 | python2 lg.py 61 | python2 lgproxy.py 62 | 63 | Systemd unit files are provided in the `init/` subdirectory. 64 | 65 | 66 | Configuration 67 | ------------- 68 | 69 | On your routers, copy `lgproxy.cfg.example` to `lgproxy.cfg` and edit the values. 70 | 71 | On the web host, copy `lg.cfg.example` to `lg.cfg` and edit the values. 72 | 73 | 74 | License 75 | ------- 76 | 77 | Source code is under GPL 3.0, powered by Flask, jQuery and Bootstrap. 78 | 79 | Copyright © 2012 Mehdi Abaakouk 80 | 81 | Happy users 82 | ----------- 83 | 84 | * https://lg.ovh.net/ 85 | * http://lg.beta.as6453.net/ 86 | * https://lg.hamburg.freifunk.net/start 87 | * http://lg.ring.nlnog.net/ 88 | * https://lg.tetaneutral.net/ 89 | * https://lg.gitoyen.net/ 90 | * http://lg.as5580.net/ 91 | * https://lg.ldn-fai.net/ 92 | * http://lg.arn-fai.net 93 | * https://lg.grenode.net/ 94 | * http://lg.dataix.ru/ 95 | * https://lg.blix.com/ 96 | * https://lg.man-da.de/ 97 | * http://route-server.belwue.net/ 98 | * https://lg.exn.uk/ 99 | * https://meerblick.io/ 100 | * https://lg.as49697.net/ 101 | * http://lg.netnation.com/ 102 | * http://lg.edxnetwork.eu/ 103 | * https://lg.hivane.net/ 104 | * https://atw.hu/looking-glass 105 | * http://lg.sibir-ix.ru/ 106 | * http://lg.interlan.ro/ 107 | * http://lg.as35266.net/ 108 | * https://lg.atw.co.hu/ 109 | * http://lg.as60362.net/ 110 | * http://lg.stuttgart-ix.de/ 111 | * http://www.bet3000.tv/ 112 | * https://lg.franceix.net/ 113 | * https://lg.fullsave.net/ 114 | * http://lg.catnix.net/ 115 | * https://lg.worldstream.nl/ 116 | * https://lg.angolacables.co.ao/ 117 | -------------------------------------------------------------------------------- /lgproxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # vim: ts=4 4 | ### 5 | # 6 | # Copyright (c) 2006 Mehdi Abaakouk 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 3 as 10 | # published by the Free Software Foundation 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA 20 | # 21 | ### 22 | 23 | 24 | import sys 25 | import logging 26 | from logging.handlers import TimedRotatingFileHandler 27 | from logging import FileHandler 28 | import subprocess 29 | from urllib import unquote 30 | import argparse 31 | 32 | from bird import BirdSocket 33 | 34 | from flask import Flask, request, abort 35 | 36 | parser = argparse.ArgumentParser() 37 | parser.add_argument('-c', dest='config_file', help='path to config file', default='lgproxy.cfg') 38 | args = parser.parse_args() 39 | 40 | app = Flask(__name__) 41 | app.debug = app.config["DEBUG"] 42 | app.config.from_pyfile(args.config_file) 43 | 44 | file_handler = TimedRotatingFileHandler(filename=app.config["LOG_FILE"], when="midnight", backupCount=app.config.get("LOG_NUM_DAYS", 0)) 45 | app.logger.setLevel(getattr(logging, app.config["LOG_LEVEL"].upper())) 46 | app.logger.addHandler(file_handler) 47 | 48 | @app.before_request 49 | def access_log_before(*args, **kwargs): 50 | app.logger.info("[%s] request %s, %s", request.remote_addr, request.url, "|".join(["%s:%s"%(k,v) for k,v in request.headers.items()])) 51 | 52 | @app.after_request 53 | def access_log_after(response, *args, **kwargs): 54 | app.logger.info("[%s] reponse %s, %s", request.remote_addr, request.url, response.status_code) 55 | return response 56 | 57 | def check_security(): 58 | if app.config["ACCESS_LIST"] and request.remote_addr not in app.config["ACCESS_LIST"]: 59 | app.logger.info("Your remote address is not valid") 60 | abort(401) 61 | 62 | if app.config.get('SHARED_SECRET') and request.args.get("secret") != app.config["SHARED_SECRET"]: 63 | app.logger.info("Your shared secret is not valid") 64 | abort(401) 65 | 66 | @app.route("/traceroute") 67 | @app.route("/traceroute6") 68 | def traceroute(): 69 | check_security() 70 | 71 | if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd') or sys.platform.startswith('openbsd'): 72 | traceroute4 = [ 'traceroute' ] 73 | traceroute6 = [ 'traceroute6' ] 74 | else: # For Linux 75 | traceroute4 = [ 'traceroute', '-4' ] 76 | traceroute6 = [ 'traceroute', '-6' ] 77 | 78 | src = [] 79 | if request.path == '/traceroute6': 80 | traceroute = traceroute6 81 | if app.config.get("IPV6_SOURCE", ""): 82 | src = [ "-s", app.config.get("IPV6_SOURCE") ] 83 | else: 84 | traceroute = traceroute4 85 | if app.config.get("IPV4_SOURCE",""): 86 | src = [ "-s", app.config.get("IPV4_SOURCE") ] 87 | 88 | query = request.args.get("q","") 89 | query = unquote(query) 90 | 91 | if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd'): 92 | options = [ '-a', '-q1', '-w1', '-m15' ] 93 | elif sys.platform.startswith('openbsd'): 94 | options = [ '-A', '-q1', '-w1', '-m15' ] 95 | else: # For Linux 96 | options = [ '-A', '-q1', '-N32', '-w1', '-m15' ] 97 | command = traceroute + src + options + [ query ] 98 | result = subprocess.Popen( command , stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n","
") 99 | return result 100 | 101 | 102 | @app.route("/bird") 103 | @app.route("/bird6") 104 | def bird(): 105 | check_security() 106 | 107 | if request.path == "/bird": b = BirdSocket(file=app.config.get("BIRD_SOCKET")) 108 | elif request.path == "/bird6": b = BirdSocket(file=app.config.get("BIRD6_SOCKET")) 109 | else: return "No bird socket selected" 110 | 111 | query = request.args.get("q","") 112 | query = unquote(query) 113 | 114 | status, result = b.cmd(query) 115 | b.close() 116 | # FIXME: use status 117 | return result 118 | 119 | 120 | if __name__ == "__main__": 121 | app.logger.info("lgproxy start") 122 | app.run(app.config.get("BIND_IP", "0.0.0.0"), app.config.get("BIND_PORT", 5000)) 123 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{config.WEBSITE_TITLE|default("Bird-LG / Looking Glass") }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 64 |
65 |
66 |
67 | {% if warnings %} 68 |
69 | {% for warning in warnings %}{{warning}}
{% endfor %} 70 |
71 | {% endif %} 72 | {% if errors %} 73 |
74 | {% for error in errors %}{{error}}
{% endfor %} 75 |
76 | {% endif %} 77 | 78 | {% block body %}{% endblock %} 79 | 80 |
81 |
82 |
83 | 93 |
94 |
95 |
96 | 97 | 100 | 101 | 104 | 116 |
117 | 118 | 119 | 120 | 121 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /bird.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: ts=4 3 | ### 4 | # 5 | # Copyright (c) 2006 Mehdi Abaakouk 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 3 as 9 | # published by the Free Software Foundation 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA 19 | # 20 | ### 21 | 22 | import socket 23 | import sys 24 | 25 | BUFSIZE = 4096 26 | 27 | SUCCESS_CODES = { 28 | "0000" : "OK", 29 | "0001" : "Welcome", 30 | "0002" : "Reading configuration", 31 | "0003" : "Reconfigured", 32 | "0004" : "Reconfiguration in progress", 33 | "0005" : "Reconfiguration already in progress, queueing", 34 | "0006" : "Reconfiguration ignored, shutting down", 35 | "0007" : "Shutdown ordered", 36 | "0008" : "Already disabled", 37 | "0009" : "Disabled", 38 | "0010" : "Already enabled", 39 | "0011" : "Enabled", 40 | "0012" : "Restarted", 41 | "0013" : "Status report", 42 | "0014" : "Route count", 43 | "0015" : "Reloading", 44 | "0016" : "Access restricted", 45 | } 46 | 47 | TABLES_ENTRY_CODES = { 48 | "1000" : "BIRD version", 49 | "1001" : "Interface list", 50 | "1002" : "Protocol list", 51 | "1003" : "Interface address", 52 | "1004" : "Interface flags", 53 | "1005" : "Interface summary", 54 | "1006" : "Protocol details", 55 | "1007" : "Route list", 56 | "1008" : "Route details", 57 | "1009" : "Static route list", 58 | "1010" : "Symbol list", 59 | "1011" : "Uptime", 60 | "1012" : "Route extended attribute list", 61 | "1013" : "Show ospf neighbors", 62 | "1014" : "Show ospf", 63 | "1015" : "Show ospf interface", 64 | "1016" : "Show ospf state/topology", 65 | "1017" : "Show ospf lsadb", 66 | "1018" : "Show memory", 67 | } 68 | 69 | ERROR_CODES = { 70 | "8000" : "Reply too long", 71 | "8001" : "Route not found", 72 | "8002" : "Configuration file error", 73 | "8003" : "No protocols match", 74 | "8004" : "Stopped due to reconfiguration", 75 | "8005" : "Protocol is down => cannot dump", 76 | "8006" : "Reload failed", 77 | "8007" : "Access denied", 78 | 79 | "9000" : "Command too long", 80 | "9001" : "Parse error", 81 | "9002" : "Invalid symbol type", 82 | } 83 | 84 | END_CODES = ERROR_CODES.keys() + SUCCESS_CODES.keys() 85 | 86 | global bird_sockets 87 | bird_sockets = {} 88 | 89 | def BirdSocketSingleton(host, port): 90 | global bird_sockets 91 | s = bird_sockets.get((host,port), None) 92 | if not s: 93 | s = BirdSocket(host,port) 94 | bird_sockets[(host,port)] = s 95 | return s 96 | 97 | class BirdSocket: 98 | 99 | def __init__(self, host="", port="", file=""): 100 | self.__file = file 101 | self.__host = host 102 | self.__port = port 103 | self.__sock = None 104 | 105 | def __connect(self): 106 | if self.__sock: return 107 | 108 | if not file: 109 | self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 110 | self.__sock.settimeout(3.0) 111 | self.__sock.connect((self.__host, self.__port)) 112 | else: 113 | self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 114 | self.__sock.settimeout(3.0) 115 | self.__sock.connect(self.__file) 116 | 117 | # read welcome message 118 | self.__sock.recv(1024) 119 | self.cmd("restrict") 120 | 121 | def close(self): 122 | if self.__sock: 123 | try: self.__sock.close() 124 | except: pass 125 | self.__sock = None 126 | 127 | def cmd(self, cmd): 128 | try: 129 | self.__connect() 130 | self.__sock.send(cmd + "\n") 131 | data = self.__read() 132 | return data 133 | except socket.error: 134 | why = sys.exc_info()[1] 135 | self.close() 136 | return False, "Bird connection problem: %s" % why 137 | 138 | def __read(self): 139 | code = "7000" # Not used in bird 140 | parsed_string = "" 141 | lastline = "" 142 | 143 | while code not in END_CODES: 144 | data = self.__sock.recv(BUFSIZE) 145 | 146 | lines = (lastline + data).split("\n") 147 | if len(data) == BUFSIZE: 148 | lastline = lines[-1] 149 | lines = lines[:-1] 150 | 151 | for line in lines: 152 | code = line[0:4] 153 | 154 | if not line.strip(): 155 | continue 156 | elif code == "0000": 157 | return True, parsed_string 158 | elif code in SUCCESS_CODES.keys(): 159 | return True, SUCCESS_CODES.get(code) 160 | elif code in ERROR_CODES.keys(): 161 | return False, ERROR_CODES.get(code) 162 | elif code[0] in [ "1", "2"] : 163 | parsed_string += line[5:] + "\n" 164 | elif code[0] == " ": 165 | parsed_string += line[1:] + "\n" 166 | elif code[0] == "+": 167 | parsed_string += line[1:] 168 | else: 169 | parsed_string += "<<>>\n"%line 170 | 171 | return True, parsed_string 172 | 173 | 174 | __all__ = ['BirdSocketSingleton', 'BirdSocket'] 175 | -------------------------------------------------------------------------------- /static/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | 2 | .hidden{display:none;visibility:hidden;} 3 | @media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:18px;} input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} .input-prepend input[class*="span"],.input-append input[class*="span"]{width:auto;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{position:absolute;top:10px;left:10px;right:10px;width:auto;margin:0;}.modal.fade.in{top:auto;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (max-width:768px){.container{width:auto;padding:0 20px;} .row-fluid{width:100%;} .row{margin-left:0;} .row>[class*="span"],.row-fluid>[class*="span"]{float:none;display:block;width:auto;margin:0;}}@media (min-width:768px) and (max-width:980px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:20px;} .span1{width:42px;} .span2{width:104px;} .span3{width:166px;} .span4{width:228px;} .span5{width:290px;} .span6{width:352px;} .span7{width:414px;} .span8{width:476px;} .span9{width:538px;} .span10{width:600px;} .span11{width:662px;} .span12,.container{width:724px;} .offset1{margin-left:82px;} .offset2{margin-left:144px;} .offset3{margin-left:206px;} .offset4{margin-left:268px;} .offset5{margin-left:330px;} .offset6{margin-left:392px;} .offset7{margin-left:454px;} .offset8{margin-left:516px;} .offset9{margin-left:578px;} .offset10{margin-left:640px;} .offset11{margin-left:702px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.762430939%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid .span1{width:5.801104972%;} .row-fluid .span2{width:14.364640883%;} .row-fluid .span3{width:22.928176794%;} .row-fluid .span4{width:31.491712705%;} .row-fluid .span5{width:40.055248616%;} .row-fluid .span6{width:48.618784527%;} .row-fluid .span7{width:57.182320438000005%;} .row-fluid .span8{width:65.74585634900001%;} .row-fluid .span9{width:74.30939226%;} .row-fluid .span10{width:82.87292817100001%;} .row-fluid .span11{width:91.436464082%;} .row-fluid .span12{width:99.999999993%;} input.span1,textarea.span1,.uneditable-input.span1{width:32px;} input.span2,textarea.span2,.uneditable-input.span2{width:94px;} input.span3,textarea.span3,.uneditable-input.span3{width:156px;} input.span4,textarea.span4,.uneditable-input.span4{width:218px;} input.span5,textarea.span5,.uneditable-input.span5{width:280px;} input.span6,textarea.span6,.uneditable-input.span6{width:342px;} input.span7,textarea.span7,.uneditable-input.span7{width:404px;} input.span8,textarea.span8,.uneditable-input.span8{width:466px;} input.span9,textarea.span9,.uneditable-input.span9{width:528px;} input.span10,textarea.span10,.uneditable-input.span10{width:590px;} input.span11,textarea.span11,.uneditable-input.span11{width:652px;} input.span12,textarea.span12,.uneditable-input.span12{width:714px;}}@media (max-width:980px){body{padding-top:0;} .navbar-fixed-top{position:static;margin-bottom:18px;} .navbar-fixed-top .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .navbar .nav-collapse{clear:left;} .navbar .nav{float:none;margin:0 0 9px;} .navbar .nav>li{float:none;} .navbar .nav>li>a{margin-bottom:2px;} .navbar .nav>.divider-vertical{display:none;} .navbar .nav>li>a,.navbar .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .navbar .dropdown-menu li+li a{margin-bottom:2px;} .navbar .nav>li>a:hover,.navbar .dropdown-menu a:hover{background-color:#222222;} .navbar .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .navbar .dropdown-menu:before,.navbar .dropdown-menu:after{display:none;} .navbar .dropdown-menu .divider{display:none;} .navbar-form,.navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222222;border-bottom:1px solid #222222;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);} .navbar .nav.pull-right{float:none;margin-left:0;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;} .btn-navbar{display:block;} .nav-collapse{overflow:hidden;height:0;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:30px;} .span1{width:70px;} .span2{width:170px;} .span3{width:270px;} .span4{width:370px;} .span5{width:470px;} .span6{width:570px;} .span7{width:670px;} .span8{width:770px;} .span9{width:870px;} .span10{width:970px;} .span11{width:1070px;} .span12,.container{width:1170px;} .offset1{margin-left:130px;} .offset2{margin-left:230px;} .offset3{margin-left:330px;} .offset4{margin-left:430px;} .offset5{margin-left:530px;} .offset6{margin-left:630px;} .offset7{margin-left:730px;} .offset8{margin-left:830px;} .offset9{margin-left:930px;} .offset10{margin-left:1030px;} .offset11{margin-left:1130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.564102564%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid .span1{width:5.982905983%;} .row-fluid .span2{width:14.529914530000001%;} .row-fluid .span3{width:23.076923077%;} .row-fluid .span4{width:31.623931624%;} .row-fluid .span5{width:40.170940171000005%;} .row-fluid .span6{width:48.717948718%;} .row-fluid .span7{width:57.264957265%;} .row-fluid .span8{width:65.81196581200001%;} .row-fluid .span9{width:74.358974359%;} .row-fluid .span10{width:82.905982906%;} .row-fluid .span11{width:91.45299145300001%;} .row-fluid .span12{width:100%;} input.span1,textarea.span1,.uneditable-input.span1{width:60px;} input.span2,textarea.span2,.uneditable-input.span2{width:160px;} input.span3,textarea.span3,.uneditable-input.span3{width:260px;} input.span4,textarea.span4,.uneditable-input.span4{width:360px;} input.span5,textarea.span5,.uneditable-input.span5{width:460px;} input.span6,textarea.span6,.uneditable-input.span6{width:560px;} input.span7,textarea.span7,.uneditable-input.span7{width:660px;} input.span8,textarea.span8,.uneditable-input.span8{width:760px;} input.span9,textarea.span9,.uneditable-input.span9{width:860px;} input.span10,textarea.span10,.uneditable-input.span10{width:960px;} input.span11,textarea.span11,.uneditable-input.span11{width:1060px;} input.span12,textarea.span12,.uneditable-input.span12{width:1160px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;}} 4 | -------------------------------------------------------------------------------- /static/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.0 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .hidden { 11 | display: none; 12 | visibility: hidden; 13 | } 14 | @media (max-width: 480px) { 15 | .nav-collapse { 16 | -webkit-transform: translate3d(0, 0, 0); 17 | } 18 | .page-header h1 small { 19 | display: block; 20 | line-height: 18px; 21 | } 22 | input[class*="span"], 23 | select[class*="span"], 24 | textarea[class*="span"], 25 | .uneditable-input { 26 | display: block; 27 | width: 100%; 28 | height: 28px; 29 | /* Make inputs at least the height of their button counterpart */ 30 | 31 | /* Makes inputs behave like true block-level elements */ 32 | 33 | -webkit-box-sizing: border-box; 34 | /* Older Webkit */ 35 | 36 | -moz-box-sizing: border-box; 37 | /* Older FF */ 38 | 39 | -ms-box-sizing: border-box; 40 | /* IE8 */ 41 | 42 | box-sizing: border-box; 43 | /* CSS3 spec*/ 44 | 45 | } 46 | .input-prepend input[class*="span"], .input-append input[class*="span"] { 47 | width: auto; 48 | } 49 | input[type="checkbox"], input[type="radio"] { 50 | border: 1px solid #ccc; 51 | } 52 | .form-horizontal .control-group > label { 53 | float: none; 54 | width: auto; 55 | padding-top: 0; 56 | text-align: left; 57 | } 58 | .form-horizontal .controls { 59 | margin-left: 0; 60 | } 61 | .form-horizontal .control-list { 62 | padding-top: 0; 63 | } 64 | .form-horizontal .form-actions { 65 | padding-left: 10px; 66 | padding-right: 10px; 67 | } 68 | .modal { 69 | position: absolute; 70 | top: 10px; 71 | left: 10px; 72 | right: 10px; 73 | width: auto; 74 | margin: 0; 75 | } 76 | .modal.fade.in { 77 | top: auto; 78 | } 79 | .modal-header .close { 80 | padding: 10px; 81 | margin: -10px; 82 | } 83 | .carousel-caption { 84 | position: static; 85 | } 86 | } 87 | @media (max-width: 768px) { 88 | .container { 89 | width: auto; 90 | padding: 0 20px; 91 | } 92 | .row-fluid { 93 | width: 100%; 94 | } 95 | .row { 96 | margin-left: 0; 97 | } 98 | .row > [class*="span"], .row-fluid > [class*="span"] { 99 | float: none; 100 | display: block; 101 | width: auto; 102 | margin: 0; 103 | } 104 | } 105 | @media (min-width: 768px) and (max-width: 980px) { 106 | .row { 107 | margin-left: -20px; 108 | *zoom: 1; 109 | } 110 | .row:before, .row:after { 111 | display: table; 112 | content: ""; 113 | } 114 | .row:after { 115 | clear: both; 116 | } 117 | [class*="span"] { 118 | float: left; 119 | margin-left: 20px; 120 | } 121 | .span1 { 122 | width: 42px; 123 | } 124 | .span2 { 125 | width: 104px; 126 | } 127 | .span3 { 128 | width: 166px; 129 | } 130 | .span4 { 131 | width: 228px; 132 | } 133 | .span5 { 134 | width: 290px; 135 | } 136 | .span6 { 137 | width: 352px; 138 | } 139 | .span7 { 140 | width: 414px; 141 | } 142 | .span8 { 143 | width: 476px; 144 | } 145 | .span9 { 146 | width: 538px; 147 | } 148 | .span10 { 149 | width: 600px; 150 | } 151 | .span11 { 152 | width: 662px; 153 | } 154 | .span12, .container { 155 | width: 724px; 156 | } 157 | .offset1 { 158 | margin-left: 82px; 159 | } 160 | .offset2 { 161 | margin-left: 144px; 162 | } 163 | .offset3 { 164 | margin-left: 206px; 165 | } 166 | .offset4 { 167 | margin-left: 268px; 168 | } 169 | .offset5 { 170 | margin-left: 330px; 171 | } 172 | .offset6 { 173 | margin-left: 392px; 174 | } 175 | .offset7 { 176 | margin-left: 454px; 177 | } 178 | .offset8 { 179 | margin-left: 516px; 180 | } 181 | .offset9 { 182 | margin-left: 578px; 183 | } 184 | .offset10 { 185 | margin-left: 640px; 186 | } 187 | .offset11 { 188 | margin-left: 702px; 189 | } 190 | .row-fluid { 191 | width: 100%; 192 | *zoom: 1; 193 | } 194 | .row-fluid:before, .row-fluid:after { 195 | display: table; 196 | content: ""; 197 | } 198 | .row-fluid:after { 199 | clear: both; 200 | } 201 | .row-fluid > [class*="span"] { 202 | float: left; 203 | margin-left: 2.762430939%; 204 | } 205 | .row-fluid > [class*="span"]:first-child { 206 | margin-left: 0; 207 | } 208 | .row-fluid .span1 { 209 | width: 5.801104972%; 210 | } 211 | .row-fluid .span2 { 212 | width: 14.364640883%; 213 | } 214 | .row-fluid .span3 { 215 | width: 22.928176794%; 216 | } 217 | .row-fluid .span4 { 218 | width: 31.491712705%; 219 | } 220 | .row-fluid .span5 { 221 | width: 40.055248616%; 222 | } 223 | .row-fluid .span6 { 224 | width: 48.618784527%; 225 | } 226 | .row-fluid .span7 { 227 | width: 57.182320438000005%; 228 | } 229 | .row-fluid .span8 { 230 | width: 65.74585634900001%; 231 | } 232 | .row-fluid .span9 { 233 | width: 74.30939226%; 234 | } 235 | .row-fluid .span10 { 236 | width: 82.87292817100001%; 237 | } 238 | .row-fluid .span11 { 239 | width: 91.436464082%; 240 | } 241 | .row-fluid .span12 { 242 | width: 99.999999993%; 243 | } 244 | input.span1, textarea.span1, .uneditable-input.span1 { 245 | width: 32px; 246 | } 247 | input.span2, textarea.span2, .uneditable-input.span2 { 248 | width: 94px; 249 | } 250 | input.span3, textarea.span3, .uneditable-input.span3 { 251 | width: 156px; 252 | } 253 | input.span4, textarea.span4, .uneditable-input.span4 { 254 | width: 218px; 255 | } 256 | input.span5, textarea.span5, .uneditable-input.span5 { 257 | width: 280px; 258 | } 259 | input.span6, textarea.span6, .uneditable-input.span6 { 260 | width: 342px; 261 | } 262 | input.span7, textarea.span7, .uneditable-input.span7 { 263 | width: 404px; 264 | } 265 | input.span8, textarea.span8, .uneditable-input.span8 { 266 | width: 466px; 267 | } 268 | input.span9, textarea.span9, .uneditable-input.span9 { 269 | width: 528px; 270 | } 271 | input.span10, textarea.span10, .uneditable-input.span10 { 272 | width: 590px; 273 | } 274 | input.span11, textarea.span11, .uneditable-input.span11 { 275 | width: 652px; 276 | } 277 | input.span12, textarea.span12, .uneditable-input.span12 { 278 | width: 714px; 279 | } 280 | } 281 | @media (max-width: 980px) { 282 | body { 283 | padding-top: 0; 284 | } 285 | .navbar-fixed-top { 286 | position: static; 287 | margin-bottom: 18px; 288 | } 289 | .navbar-fixed-top .navbar-inner { 290 | padding: 5px; 291 | } 292 | .navbar .container { 293 | width: auto; 294 | padding: 0; 295 | } 296 | .navbar .brand { 297 | padding-left: 10px; 298 | padding-right: 10px; 299 | margin: 0 0 0 -5px; 300 | } 301 | .navbar .nav-collapse { 302 | clear: left; 303 | } 304 | .navbar .nav { 305 | float: none; 306 | margin: 0 0 9px; 307 | } 308 | .navbar .nav > li { 309 | float: none; 310 | } 311 | .navbar .nav > li > a { 312 | margin-bottom: 2px; 313 | } 314 | .navbar .nav > .divider-vertical { 315 | display: none; 316 | } 317 | .navbar .nav > li > a, .navbar .dropdown-menu a { 318 | padding: 6px 15px; 319 | font-weight: bold; 320 | color: #999999; 321 | -webkit-border-radius: 3px; 322 | -moz-border-radius: 3px; 323 | border-radius: 3px; 324 | } 325 | .navbar .dropdown-menu li + li a { 326 | margin-bottom: 2px; 327 | } 328 | .navbar .nav > li > a:hover, .navbar .dropdown-menu a:hover { 329 | background-color: #222222; 330 | } 331 | .navbar .dropdown-menu { 332 | position: static; 333 | top: auto; 334 | left: auto; 335 | float: none; 336 | display: block; 337 | max-width: none; 338 | margin: 0 15px; 339 | padding: 0; 340 | background-color: transparent; 341 | border: none; 342 | -webkit-border-radius: 0; 343 | -moz-border-radius: 0; 344 | border-radius: 0; 345 | -webkit-box-shadow: none; 346 | -moz-box-shadow: none; 347 | box-shadow: none; 348 | } 349 | .navbar .dropdown-menu:before, .navbar .dropdown-menu:after { 350 | display: none; 351 | } 352 | .navbar .dropdown-menu .divider { 353 | display: none; 354 | } 355 | .navbar-form, .navbar-search { 356 | float: none; 357 | padding: 9px 15px; 358 | margin: 9px 0; 359 | border-top: 1px solid #222222; 360 | border-bottom: 1px solid #222222; 361 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 362 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 363 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 364 | } 365 | .navbar .nav.pull-right { 366 | float: none; 367 | margin-left: 0; 368 | } 369 | .navbar-static .navbar-inner { 370 | padding-left: 10px; 371 | padding-right: 10px; 372 | } 373 | .btn-navbar { 374 | display: block; 375 | } 376 | .nav-collapse { 377 | overflow: hidden; 378 | height: 0; 379 | } 380 | } 381 | @media (min-width: 980px) { 382 | .nav-collapse.collapse { 383 | height: auto !important; 384 | } 385 | } 386 | @media (min-width: 1200px) { 387 | .row { 388 | margin-left: -30px; 389 | *zoom: 1; 390 | } 391 | .row:before, .row:after { 392 | display: table; 393 | content: ""; 394 | } 395 | .row:after { 396 | clear: both; 397 | } 398 | [class*="span"] { 399 | float: left; 400 | margin-left: 30px; 401 | } 402 | .span1 { 403 | width: 70px; 404 | } 405 | .span2 { 406 | width: 170px; 407 | } 408 | .span3 { 409 | width: 270px; 410 | } 411 | .span4 { 412 | width: 370px; 413 | } 414 | .span5 { 415 | width: 470px; 416 | } 417 | .span6 { 418 | width: 570px; 419 | } 420 | .span7 { 421 | width: 670px; 422 | } 423 | .span8 { 424 | width: 770px; 425 | } 426 | .span9 { 427 | width: 870px; 428 | } 429 | .span10 { 430 | width: 970px; 431 | } 432 | .span11 { 433 | width: 1070px; 434 | } 435 | .span12, .container { 436 | width: 1170px; 437 | } 438 | .offset1 { 439 | margin-left: 130px; 440 | } 441 | .offset2 { 442 | margin-left: 230px; 443 | } 444 | .offset3 { 445 | margin-left: 330px; 446 | } 447 | .offset4 { 448 | margin-left: 430px; 449 | } 450 | .offset5 { 451 | margin-left: 530px; 452 | } 453 | .offset6 { 454 | margin-left: 630px; 455 | } 456 | .offset7 { 457 | margin-left: 730px; 458 | } 459 | .offset8 { 460 | margin-left: 830px; 461 | } 462 | .offset9 { 463 | margin-left: 930px; 464 | } 465 | .offset10 { 466 | margin-left: 1030px; 467 | } 468 | .offset11 { 469 | margin-left: 1130px; 470 | } 471 | .row-fluid { 472 | width: 100%; 473 | *zoom: 1; 474 | } 475 | .row-fluid:before, .row-fluid:after { 476 | display: table; 477 | content: ""; 478 | } 479 | .row-fluid:after { 480 | clear: both; 481 | } 482 | .row-fluid > [class*="span"] { 483 | float: left; 484 | margin-left: 2.564102564%; 485 | } 486 | .row-fluid > [class*="span"]:first-child { 487 | margin-left: 0; 488 | } 489 | .row-fluid .span1 { 490 | width: 5.982905983%; 491 | } 492 | .row-fluid .span2 { 493 | width: 14.529914530000001%; 494 | } 495 | .row-fluid .span3 { 496 | width: 23.076923077%; 497 | } 498 | .row-fluid .span4 { 499 | width: 31.623931624%; 500 | } 501 | .row-fluid .span5 { 502 | width: 40.170940171000005%; 503 | } 504 | .row-fluid .span6 { 505 | width: 48.717948718%; 506 | } 507 | .row-fluid .span7 { 508 | width: 57.264957265%; 509 | } 510 | .row-fluid .span8 { 511 | width: 65.81196581200001%; 512 | } 513 | .row-fluid .span9 { 514 | width: 74.358974359%; 515 | } 516 | .row-fluid .span10 { 517 | width: 82.905982906%; 518 | } 519 | .row-fluid .span11 { 520 | width: 91.45299145300001%; 521 | } 522 | .row-fluid .span12 { 523 | width: 100%; 524 | } 525 | input.span1, textarea.span1, .uneditable-input.span1 { 526 | width: 60px; 527 | } 528 | input.span2, textarea.span2, .uneditable-input.span2 { 529 | width: 160px; 530 | } 531 | input.span3, textarea.span3, .uneditable-input.span3 { 532 | width: 260px; 533 | } 534 | input.span4, textarea.span4, .uneditable-input.span4 { 535 | width: 360px; 536 | } 537 | input.span5, textarea.span5, .uneditable-input.span5 { 538 | width: 460px; 539 | } 540 | input.span6, textarea.span6, .uneditable-input.span6 { 541 | width: 560px; 542 | } 543 | input.span7, textarea.span7, .uneditable-input.span7 { 544 | width: 660px; 545 | } 546 | input.span8, textarea.span8, .uneditable-input.span8 { 547 | width: 760px; 548 | } 549 | input.span9, textarea.span9, .uneditable-input.span9 { 550 | width: 860px; 551 | } 552 | input.span10, textarea.span10, .uneditable-input.span10 { 553 | width: 960px; 554 | } 555 | input.span11, textarea.span11, .uneditable-input.span11 { 556 | width: 1060px; 557 | } 558 | input.span12, textarea.span12, .uneditable-input.span12 { 559 | width: 1160px; 560 | } 561 | .thumbnails { 562 | margin-left: -30px; 563 | } 564 | .thumbnails > li { 565 | margin-left: 30px; 566 | } 567 | } 568 | -------------------------------------------------------------------------------- /static/css/docs.css: -------------------------------------------------------------------------------- 1 | /* Add additional stylesheets below 2 | -------------------------------------------------- */ 3 | /* 4 | Bootstrap's documentation styles 5 | Special styles for presenting Bootstrap's documentation and examples 6 | */ 7 | 8 | 9 | /* Body and structure 10 | -------------------------------------------------- */ 11 | body { 12 | position: relative; 13 | padding-top: 68px; 14 | background-color: #fff; 15 | background-repeat: repeat-x; 16 | background-position: 0 40px; 17 | } 18 | 19 | 20 | /* Tweak navbar brand link to be super sleek 21 | -------------------------------------------------- */ 22 | .navbar-fixed-top .brand { 23 | padding-top: 12px; 24 | padding-right: 0; 25 | padding-left: 0; 26 | margin-left: 20px; 27 | float: right; 28 | font-weight: bold; 29 | color: #000; 30 | text-shadow: 0 1px 0 rgba(255,255,255,.1), 0 0 30px rgba(255,255,255,.125); 31 | -webkit-transition: all .2s linear; 32 | -moz-transition: all .2s linear; 33 | transition: all .2s linear; 34 | } 35 | .navbar-fixed-top .brand:hover { 36 | text-decoration: none; 37 | } 38 | 39 | 40 | /* Space out sub-sections more 41 | -------------------------------------------------- */ 42 | section { 43 | padding-top: 60px; 44 | } 45 | 46 | /* Faded out hr */ 47 | hr.soften { 48 | height: 1px; 49 | margin: 54px 0; 50 | background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 51 | background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 52 | background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 53 | background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 54 | border: 0; 55 | } 56 | 57 | 58 | /* Jumbotrons 59 | -------------------------------------------------- */ 60 | .jumbotron { 61 | position: relative; 62 | } 63 | .jumbotron h1 { 64 | margin-bottom: 9px; 65 | font-size: 81px; 66 | letter-spacing: -1px; 67 | line-height: 1; 68 | } 69 | .jumbotron p { 70 | margin-bottom: 18px; 71 | font-weight: 300; 72 | } 73 | .jumbotron .btn-large { 74 | font-size: 20px; 75 | font-weight: normal; 76 | padding: 14px 24px; 77 | margin-right: 10px; 78 | -webkit-border-radius: 6px; 79 | -moz-border-radius: 6px; 80 | border-radius: 6px; 81 | } 82 | 83 | /* Masthead (docs home) */ 84 | .masthead { 85 | padding-top: 36px; 86 | margin-bottom: 72px; 87 | } 88 | .masthead h1, 89 | .masthead p { 90 | text-align: center; 91 | } 92 | .masthead h1 { 93 | margin-bottom: 18px; 94 | } 95 | .masthead p { 96 | margin-left: 5%; 97 | margin-right: 5%; 98 | font-size: 30px; 99 | line-height: 36px; 100 | } 101 | 102 | 103 | /* Specific jumbotrons 104 | ------------------------- */ 105 | /* supporting docs pages */ 106 | .subhead { 107 | padding-bottom: 0; 108 | margin-bottom: 9px; 109 | } 110 | .subhead h1 { 111 | font-size: 54px; 112 | } 113 | 114 | /* Subnav */ 115 | .subnav { 116 | width: 100%; 117 | height: 36px; 118 | background-color: #eeeeee; /* Old browsers */ 119 | background-repeat: repeat-x; /* Repeat the gradient */ 120 | background-image: -moz-linear-gradient(top, #f5f5f5 0%, #eeeeee 100%); /* FF3.6+ */ 121 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f5f5f5), color-stop(100%,#eeeeee)); /* Chrome,Safari4+ */ 122 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%,#eeeeee 100%); /* Chrome 10+,Safari 5.1+ */ 123 | background-image: -ms-linear-gradient(top, #f5f5f5 0%,#eeeeee 100%); /* IE10+ */ 124 | background-image: -o-linear-gradient(top, #f5f5f5 0%,#eeeeee 100%); /* Opera 11.10+ */ 125 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f5f5f5', endColorstr='#eeeeee',GradientType=0 ); /* IE6-9 */ 126 | background-image: linear-gradient(top, #f5f5f5 0%,#eeeeee 100%); /* W3C */ 127 | border: 1px solid #e5e5e5; 128 | -webkit-border-radius: 4px; 129 | -moz-border-radius: 4px; 130 | border-radius: 4px; 131 | } 132 | .subnav .nav { 133 | margin-bottom: 0; 134 | } 135 | .subnav .nav > li > a { 136 | margin: 0; 137 | padding-top: 11px; 138 | padding-bottom: 11px; 139 | border-left: 1px solid #f5f5f5; 140 | border-right: 1px solid #e5e5e5; 141 | -webkit-border-radius: 0; 142 | -moz-border-radius: 0; 143 | border-radius: 0; 144 | } 145 | .subnav .nav > .active > a, 146 | .subnav .nav > .active > a:hover { 147 | padding-left: 13px; 148 | color: #777; 149 | background-color: #e9e9e9; 150 | border-right-color: #ddd; 151 | border-left: 0; 152 | -webkit-box-shadow: inset 0 3px 5px rgba(0,0,0,.05); 153 | -moz-box-shadow: inset 0 3px 5px rgba(0,0,0,.05); 154 | box-shadow: inset 0 3px 5px rgba(0,0,0,.05); 155 | } 156 | .subnav .nav > .active > a .caret, 157 | .subnav .nav > .active > a:hover .caret { 158 | border-top-color: #777; 159 | } 160 | .subnav .nav > li:first-child > a, 161 | .subnav .nav > li:first-child > a:hover { 162 | border-left: 0; 163 | padding-left: 12px; 164 | -webkit-border-radius: 4px 0 0 4px; 165 | -moz-border-radius: 4px 0 0 4px; 166 | border-radius: 4px 0 0 4px; 167 | } 168 | .subnav .nav > li:last-child > a { 169 | border-right: 0; 170 | } 171 | .subnav .dropdown-menu { 172 | -webkit-border-radius: 0 0 4px 4px; 173 | -moz-border-radius: 0 0 4px 4px; 174 | border-radius: 0 0 4px 4px; 175 | } 176 | 177 | /* Fixed subnav on scroll, but only for 980px and up (sorry IE!) */ 178 | @media (min-width: 980px) { 179 | .subnav-fixed { 180 | position: fixed; 181 | top: 40px; 182 | left: 0; 183 | right: 0; 184 | z-index: 1030; 185 | border-color: #d5d5d5; 186 | border-width: 0 0 1px; /* drop the border on the fixed edges */ 187 | -webkit-border-radius: 0; 188 | -moz-border-radius: 0; 189 | border-radius: 0; 190 | -webkit-box-shadow: inset 0 1px 0 #fff, 0 1px 5px rgba(0,0,0,.1); 191 | -moz-box-shadow: inset 0 1px 0 #fff, 0 1px 5px rgba(0,0,0,.1); 192 | box-shadow: inset 0 1px 0 #fff, 0 1px 5px rgba(0,0,0,.1); 193 | } 194 | .subnav-fixed .nav { 195 | width: 938px; 196 | margin: 0 auto; 197 | padding: 0 1px; 198 | } 199 | .subnav .nav > li:first-child > a, 200 | .subnav .nav > li:first-child > a:hover { 201 | -webkit-border-radius: 0; 202 | -moz-border-radius: 0; 203 | border-radius: 0; 204 | } 205 | } 206 | 207 | 208 | /* Quick links 209 | -------------------------------------------------- */ 210 | .quick-links { 211 | min-height: 30px; 212 | padding: 5px 20px; 213 | margin: 36px 0; 214 | list-style: none; 215 | text-align: center; 216 | overflow: hidden; 217 | } 218 | .quick-links li { 219 | display: inline; 220 | margin: 0 5px; 221 | color: #999; 222 | } 223 | .quick-links .github-btn, 224 | .quick-links .tweet-btn, 225 | .quick-links .follow-btn { 226 | position: relative; 227 | top: 5px; 228 | } 229 | 230 | 231 | /* Footer 232 | -------------------------------------------------- */ 233 | .footer { 234 | margin-top: 45px; 235 | padding: 35px 0 36px; 236 | border-top: 1px solid #e5e5e5; 237 | } 238 | .footer p { 239 | margin-bottom: 0; 240 | color: #555; 241 | } 242 | 243 | 244 | 245 | /* Special grid styles 246 | -------------------------------------------------- */ 247 | .show-grid { 248 | margin-top: 10px; 249 | margin-bottom: 20px; 250 | } 251 | .show-grid [class*="span"] { 252 | background-color: #eee; 253 | text-align: center; 254 | -webkit-border-radius: 3px; 255 | -moz-border-radius: 3px; 256 | border-radius: 3px; 257 | min-height: 30px; 258 | line-height: 30px; 259 | } 260 | .show-grid:hover [class*="span"] { 261 | background: #ddd; 262 | } 263 | .show-grid .show-grid { 264 | margin-top: 0; 265 | margin-bottom: 0; 266 | } 267 | .show-grid .show-grid [class*="span"] { 268 | background-color: #ccc; 269 | } 270 | 271 | 272 | /* Render mini layout previews 273 | -------------------------------------------------- */ 274 | .mini-layout { 275 | border: 1px solid #ddd; 276 | -webkit-border-radius: 6px; 277 | -moz-border-radius: 6px; 278 | border-radius: 6px; 279 | -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.075); 280 | -moz-box-shadow: 0 1px 2px rgba(0,0,0,.075); 281 | box-shadow: 0 1px 2px rgba(0,0,0,.075); 282 | } 283 | .mini-layout { 284 | height: 240px; 285 | margin-bottom: 20px; 286 | padding: 9px; 287 | } 288 | .mini-layout div { 289 | -webkit-border-radius: 3px; 290 | -moz-border-radius: 3px; 291 | border-radius: 3px; 292 | } 293 | .mini-layout .mini-layout-body { 294 | background-color: #dceaf4; 295 | margin: 0 auto; 296 | width: 70%; 297 | height: 240px; 298 | } 299 | .mini-layout.fluid .mini-layout-sidebar, 300 | .mini-layout.fluid .mini-layout-header, 301 | .mini-layout.fluid .mini-layout-body { 302 | float: left; 303 | } 304 | .mini-layout.fluid .mini-layout-sidebar { 305 | background-color: #bbd8e9; 306 | width: 20%; 307 | height: 240px; 308 | } 309 | .mini-layout.fluid .mini-layout-body { 310 | width: 77.5%; 311 | margin-left: 2.5%; 312 | } 313 | 314 | 315 | /* Popover docs 316 | -------------------------------------------------- */ 317 | .popover-well { 318 | min-height: 160px; 319 | } 320 | .popover-well .popover { 321 | display: block; 322 | } 323 | .popover-well .popover-wrapper { 324 | width: 50%; 325 | height: 160px; 326 | float: left; 327 | margin-left: 55px; 328 | position: relative; 329 | } 330 | .popover-well .popover-menu-wrapper { 331 | height: 80px; 332 | } 333 | .large-bird { 334 | margin: 5px 0 0 310px; 335 | opacity: .1; 336 | } 337 | 338 | 339 | /* Download page 340 | -------------------------------------------------- */ 341 | .download .page-header { 342 | margin-top: 36px; 343 | } 344 | .page-header .toggle-all { 345 | margin-top: 5px; 346 | } 347 | 348 | /* Space out h3s when following a section */ 349 | .download h3 { 350 | margin-bottom: 5px; 351 | } 352 | .download-builder input + h3, 353 | .download-builder .checkbox + h3 { 354 | margin-top: 9px; 355 | } 356 | 357 | /* Fields for variables */ 358 | .download-builder input[type=text] { 359 | margin-bottom: 9px; 360 | font-family: Menlo, Monaco, "Courier New", monospace; 361 | font-size: 12px; 362 | color: #d14; 363 | } 364 | .download-builder input[type=text]:focus { 365 | background-color: #fff; 366 | } 367 | 368 | /* Custom, larger checkbox labels */ 369 | .download .checkbox { 370 | padding: 6px 10px 6px 25px; 371 | color: #555; 372 | background-color: #f9f9f9; 373 | -webkit-border-radius: 3px; 374 | -moz-border-radius: 3px; 375 | border-radius: 3px; 376 | cursor: pointer; 377 | } 378 | .download .checkbox:hover { 379 | color: #333; 380 | background-color: #f5f5f5; 381 | } 382 | .download .checkbox small { 383 | font-size: 12px; 384 | color: #777; 385 | } 386 | 387 | /* Variables section */ 388 | #variables label { 389 | margin-bottom: 0; 390 | } 391 | 392 | /* Giant download button */ 393 | .download-btn { 394 | margin: 36px 0 108px; 395 | } 396 | .download p, 397 | .download h4 { 398 | max-width: 50%; 399 | margin: 0 auto; 400 | color: #999; 401 | text-align: center; 402 | } 403 | .download h4 { 404 | margin-bottom: 0; 405 | } 406 | .download p { 407 | margin-bottom: 18px; 408 | } 409 | .download-btn .btn { 410 | display: block; 411 | width: auto; 412 | padding: 19px 24px; 413 | margin-bottom: 27px; 414 | font-size: 30px; 415 | line-height: 1; 416 | text-align: center; 417 | -webkit-border-radius: 6px; 418 | -moz-border-radius: 6px; 419 | border-radius: 6px; 420 | } 421 | 422 | 423 | 424 | /* Misc 425 | -------------------------------------------------- */ 426 | 427 | pre.prettyprint { 428 | overflow: hidden; 429 | } 430 | 431 | .browser-support { 432 | max-width: 100%; 433 | } 434 | 435 | /* Make tables spaced out a bit more */ 436 | h2 + table, 437 | h3 + table, 438 | h4 + table, 439 | h2 + .row { 440 | margin-top: 5px; 441 | } 442 | 443 | /* Example sites showcase */ 444 | .example-sites img { 445 | max-width: 100%; 446 | margin: 0 auto; 447 | } 448 | .marketing-byline { 449 | margin: -18px 0 27px; 450 | font-size: 18px; 451 | font-weight: 300; 452 | line-height: 24px; 453 | color: #999; 454 | text-align: center; 455 | } 456 | 457 | .scrollspy-example { 458 | height: 200px; 459 | overflow: auto; 460 | position: relative; 461 | } 462 | 463 | /* Remove bottom margin on example forms in wells */ 464 | form.well { 465 | padding: 14px; 466 | } 467 | 468 | /* Tighten up spacing */ 469 | .well hr { 470 | margin: 18px 0; 471 | } 472 | 473 | /* Fake the :focus state to demo it */ 474 | .focused { 475 | border-color: rgba(82,168,236,.8); 476 | -webkit-box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 477 | -moz-box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 478 | box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 479 | outline: 0; 480 | } 481 | 482 | /* For input sizes, make them display block */ 483 | .docs-input-sizes select, 484 | .docs-input-sizes input[type=text] { 485 | display: block; 486 | margin-bottom: 9px; 487 | } 488 | 489 | /* Icons 490 | ------------------------- */ 491 | .the-icons { 492 | margin-bottom: 18px; 493 | } 494 | .the-icons i { 495 | display: block; 496 | margin-bottom: 5px; 497 | } 498 | .the-icons i:hover { 499 | background-color: rgba(255,0,0,.25); 500 | } 501 | .the-icons i:after { 502 | display: block; 503 | content: attr(class); 504 | font-style: normal; 505 | margin-left: 20px; 506 | width: 140px; 507 | } 508 | #javascript input[type=checkbox] { 509 | position: relative; 510 | top: -1px; 511 | display: inline; 512 | margin-left: 6px; 513 | } 514 | 515 | /* Eaxmples page 516 | ------------------------- */ 517 | .bootstrap-examples .thumbnail { 518 | margin-bottom: 9px; 519 | background-color: #fff; 520 | } 521 | 522 | 523 | /* Responsive Docs 524 | -------------------------------------------------- */ 525 | @media (max-width: 480px) { 526 | 527 | /* Reduce padding above jumbotron */ 528 | body { 529 | padding-top: 70px; 530 | } 531 | 532 | /* Change up some type stuff */ 533 | h2 { 534 | margin-top: 27px; 535 | } 536 | h2 small { 537 | display: block; 538 | line-height: 18px; 539 | } 540 | h3 { 541 | margin-top: 18px; 542 | } 543 | 544 | /* Adjust the jumbotron */ 545 | .jumbotron h1, 546 | .jumbotron p { 547 | text-align: center; 548 | margin-right: 0; 549 | } 550 | .jumbotron h1 { 551 | font-size: 45px; 552 | margin-right: 0; 553 | } 554 | .jumbotron p { 555 | margin-right: 0; 556 | margin-left: 0; 557 | font-size: 18px; 558 | line-height: 24px; 559 | } 560 | .jumbotron .btn { 561 | display: block; 562 | font-size: 18px; 563 | padding: 10px 14px; 564 | margin: 0 auto 10px; 565 | } 566 | /* Masthead (home page jumbotron) */ 567 | .masthead { 568 | padding-top: 0; 569 | } 570 | 571 | /* Don't space out quick links so much */ 572 | .quick-links { 573 | margin: 40px 0 0; 574 | } 575 | /* hide the bullets on mobile since our horizontal space is limited */ 576 | .quick-links .divider { 577 | display: none; 578 | } 579 | 580 | /* center example sites */ 581 | .example-sites { 582 | margin-left: 0; 583 | } 584 | .example-sites > li { 585 | float: none; 586 | display: block; 587 | max-width: 280px; 588 | margin: 0 auto 18px; 589 | text-align: center; 590 | } 591 | .example-sites .thumbnail > img { 592 | max-width: 270px; 593 | } 594 | 595 | table code { 596 | white-space: normal; 597 | word-wrap: break-word; 598 | word-break: break-all; 599 | } 600 | 601 | /* Modal example */ 602 | .modal-example .modal { 603 | position: relative; 604 | top: auto; 605 | right: auto; 606 | bottom: auto; 607 | left: auto; 608 | } 609 | 610 | } 611 | 612 | 613 | @media (max-width: 768px) { 614 | 615 | /* Remove any padding from the body */ 616 | body { 617 | padding-top: 0; 618 | } 619 | 620 | /* Jumbotron buttons */ 621 | .jumbotron .btn { 622 | margin-bottom: 10px; 623 | } 624 | 625 | /* Subnav */ 626 | .subnav { 627 | position: static; 628 | top: auto; 629 | z-index: auto; 630 | width: auto; 631 | height: auto; 632 | background: #fff; /* whole background property since we use a background-image for gradient */ 633 | -webkit-box-shadow: none; 634 | -moz-box-shadow: none; 635 | box-shadow: none; 636 | } 637 | .subnav .nav > li { 638 | float: none; 639 | } 640 | .subnav .nav > li > a { 641 | border: 0; 642 | } 643 | .subnav .nav > li + li > a { 644 | border-top: 1px solid #e5e5e5; 645 | } 646 | .subnav .nav > li:first-child > a, 647 | .subnav .nav > li:first-child > a:hover { 648 | -webkit-border-radius: 4px 4px 0 0; 649 | -moz-border-radius: 4px 4px 0 0; 650 | border-radius: 4px 4px 0 0; 651 | } 652 | 653 | /* Popovers */ 654 | .large-bird { 655 | display: none; 656 | } 657 | .popover-well .popover-wrapper { 658 | margin-left: 0; 659 | } 660 | 661 | /* Space out the show-grid examples */ 662 | .show-grid [class*="span"] { 663 | margin-bottom: 5px; 664 | } 665 | 666 | /* Unfloat the back to top link in footer */ 667 | .footer .pull-right { 668 | float: none; 669 | } 670 | .footer p { 671 | margin-bottom: 9px; 672 | } 673 | 674 | } 675 | 676 | 677 | @media (min-width: 480px) and (max-width: 768px) { 678 | 679 | /* Scale down the jumbotron content */ 680 | .jumbotron h1 { 681 | font-size: 54px; 682 | } 683 | .jumbotron p { 684 | margin-right: 0; 685 | margin-left: 0; 686 | } 687 | 688 | } 689 | 690 | 691 | @media (min-width: 768px) and (max-width: 980px) { 692 | 693 | /* Remove any padding from the body */ 694 | body { 695 | padding-top: 0; 696 | } 697 | 698 | /* Scale down the jumbotron content */ 699 | .jumbotron h1 { 700 | font-size: 72px; 701 | } 702 | 703 | } 704 | 705 | 706 | @media (max-width: 980px) { 707 | 708 | /* Unfloat brand */ 709 | .navbar-fixed-top .brand { 710 | float: left; 711 | margin-left: 0; 712 | padding-left: 10px; 713 | padding-right: 10px; 714 | } 715 | 716 | /* Inline-block quick links for more spacing */ 717 | .quick-links li { 718 | display: inline-block; 719 | margin: 5px; 720 | } 721 | 722 | } 723 | 724 | 725 | /* LARGE DESKTOP SCREENS */ 726 | @media (min-width: 1210px) { 727 | 728 | /* Update subnav container */ 729 | .subnav-fixed .nav { 730 | width: 1168px; /* 2px less to account for left/right borders being removed when in fixed mode */ 731 | } 732 | 733 | } 734 | -------------------------------------------------------------------------------- /static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a(function(){"use strict",a.support.transition=function(){var b=document.body||document.documentElement,c=b.style,d=c.transition!==undefined||c.WebkitTransition!==undefined||c.MozTransition!==undefined||c.MsTransition!==undefined||c.OTransition!==undefined;return d&&{end:function(){var b="TransitionEnd";return a.browser.webkit?b="webkitTransitionEnd":a.browser.mozilla?b="transitionend":a.browser.opera&&(b="oTransitionEnd"),b}()}}()})}(window.jQuery),!function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype={constructor:c,close:function(b){function f(){e.remove(),e.trigger("closed")}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),e.trigger("close"),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()}},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype={constructor:b,setState:function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},toggle:function(){var a=this.$element.parent('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")}},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){a(b.target).button("toggle")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.carousel.defaults,c),this.options.slide&&this.slide(this.options.slide)};b.prototype={cycle:function(){return this.interval=setInterval(a.proxy(this.next,this),this.options.interval),this},to:function(b){var c=this.$element.find(".active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(){return clearInterval(this.interval),this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this;return this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h](),!a.support.transition&&this.$element.hasClass("slide")?(this.$element.trigger("slide"),d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")):(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.trigger("slide"),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})),f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=typeof c=="object"&&c;e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):typeof c=="string"||(c=f.slide)?e[c]():e.cycle()})},a.fn.carousel.defaults={interval:5e3},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find(".in"),e;d&&d.length&&(e=d.data("collapse"),d.collapse("hide"),e||d.data("collapse",null)),this.$element[b](0),this.transition("addClass","show","shown"),this.$element[b](this.$element[0][c])},hide:function(){var a=this.dimension();this.reset(this.$element[a]()),this.transition("removeClass","hide","hidden"),this.$element[a](0)},reset:function(a){var b=this.dimension();this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element.addClass("collapse")},transition:function(b,c,d){var e=this,f=function(){c=="show"&&e.reset(),e.$element.trigger(d)};this.$element.trigger(c)[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();a(e).collapse(f)})})}(window.jQuery),!function(a){function d(){a(b).parent().removeClass("open")}"use strict";var b='[data-toggle="dropdown"]',c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),e=c.attr("data-target"),f,g;return e||(e=c.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,"")),f=a(e),f.length||(f=c.parent()),g=f.hasClass("open"),d(),!g&&f.toggleClass("open"),!1}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api",d),a("body").on("click.dropdown.data-api",b,c.prototype.toggle)})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('