initial commit

This commit is contained in:
Simon Marsh 2020-10-10 18:51:45 +01:00
parent 3d9c96d01a
commit df00939d79
No known key found for this signature in database
GPG Key ID: 30B29A716A54DBB3
10 changed files with 1164 additions and 0 deletions

12
StaticRoot/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
StaticRoot/dn42_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

228
StaticRoot/grc.js Normal file
View File

@ -0,0 +1,228 @@
//////////////////////////////////////////////////////////////////////////
// DN42 Realtime GRC
//////////////////////////////////////////////////////////////////////////
const ExplorerURL='https://explorer.burble.com/#'
//////////////////////////////////////////////////////////////////////////
// smaller display components
Vue.component('reg-asn', {
template: '#reg-asn',
props: [ 'asn' ],
data() {
return { }
},
computed: {
normal: function() {
if (this.asn.startsWith("AS")) {
return this.asn;
} else {
return "AS" + this.asn;
}
},
url: function() {
return ExplorerURL + "/aut-num/" + this.normal;
}
}
})
Vue.component('reg-path', {
template: '#reg-path',
props: [ 'path' ],
data() {
return { }
},
computed: {
list: function() {
return this.path.split(" ");
}
}
})
Vue.component('reg-prefix', {
template: '#reg-prefix',
props: [ 'prefix' ],
data() {
return { }
},
computed: {
isIPv6: function() {
return this.prefix.includes(":")
},
forced: function() { return this.prefix },
url: function() {
var prefix = this.prefix.replace("/", "_")
if (this.isIPv6) {
return ExplorerURL + "/inet6num/" + prefix;
}
else {
return ExplorerURL + "/inetnum/" + prefix;
}
}
}
})
//////////////////////////////////////////////////////////////////////////
// flap list component
Vue.component('app-flaps', {
template: '#app-flaps-template',
data() {
return {
filter: '',
currentPage: 1,
total: 0,
fields: [
{
key: 'prefix',
label: 'Prefix',
sortable: true
},
{
key: 'path',
label: 'Path',
sortable: true
},
{
key: 'count',
label: 'Updates',
sortable: true,
sortDirection: 'desc'
}
],
flaps: [ ]
}
},
computed: {
rows: function() {
return this.flaps.length;
}
},
methods: {
update: function(data) {
this.flaps.splice(0);
this.total = data.total;
this.flaps = data.list.map((update) => {
var tmp = update.path.split(" ");
update.path = tmp.map((asn) => {
return "AS" + asn;
}).join(" ")
return update;
})
}
},
mounted() {
this.$root.$on('flaps-update', data => {
this.update(data)
})
}
})
//////////////////////////////////////////////////////////////////////////
// roa list component
Vue.component('app-roa', {
template: '#app-roa-template',
data() {
return {
filter4: '',
filter6: '',
roaFields: [
{
key: 'prefix',
label: 'Prefix',
sortable: true
},
{
key: 'origin',
label: 'Origin',
sortable: true
}
],
roa4: [ ],
roa6: [ ]
}
},
methods: {
update: function(data) {
this.roa4.splice(0);
this.roa6.splice(0);
data.list.forEach((roa) => {
var nroa = {
origin: "AS" + roa.origin,
prefix: roa.prefix
};
if (roa.prefix.includes(":")) { this.roa6.push(nroa) }
else { this.roa4.push(nroa) }
})
}
},
mounted() {
this.$root.$on('roa-update', data => {
this.update(data)
});
}
})
//////////////////////////////////////////////////////////////////////////
// timer to update every minute
Vue.component('app-timer', {
template: '#app-timer',
data() {
return {
seconds: 0,
timer: 0
}
},
methods: {
trigger: function() {
if (this.seconds <= 0) {
this.seconds = 61;
axios.get('/api/flaps').then(response => {
this.$root.$emit('flaps-update', response.data)
});
axios.get('/api/roa').then(response => {
this.$root.$emit('roa-update', response.data)
});
}
this.seconds -= 1;
this.timer = setTimeout(() => this.trigger(), 1000);
}
},
mounted() {
this.trigger()
}
})
//////////////////////////////////////////////////////////////////////////
// main vue application starts here
// initialise the Vue Router
const router = new VueRouter({
routes: [
{ path: '/', component: Vue.component('app-flaps') },
{ path: '/flaps', component: Vue.component('app-flaps') },
{ path: '/roa', component: Vue.component('app-roa') }
]
})
// and the main app instance
const vm = new Vue({
el: '#grc_realtime',
data: { },
router
})
//////////////////////////////////////////////////////////////////////////
// end of code

222
StaticRoot/index.html Normal file
View File

@ -0,0 +1,222 @@
<!doctype html>
<html lang="en">
<head>
<title>DN42 GRC Realtime Data</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Styles -->
<link type="text/css" rel="stylesheet" href="bootstrap.min.css">
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<link type="text/css" rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<!-- Style overrides -->
<style>
.material-icons { display:inline-flex;vertical-align:middle }
body { box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh }
</style>
<!-- Bootstrap/Vue JS -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue-router@latest/dist/vue-router.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
<!-- Other JS -->
<script src="https://unpkg.com/axios@0.20.0/dist/axios.min.js"></script>
</head>
<body>
<div id="grc_realtime">
<!-- Top Navbar -->
<nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark">
<div id="navlinks" class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto text=center">
<li class="nav-item"><a href="/" class="nav-link">Realtime&nbsp;Data</a></li>
<li class="nav-item"><a href="https://grc.burble.com" class="nav-link">GRC</a></li>
<li class="nav-item"><a href="https://explorer.burble.com" class="nav-link">Registry</a></li>
</ul>
</div>
<div class="collapse navbar-collapse w-100 ml-auto text-nowrap">
<div class="ml-auto"><router-link class="navbar-brand"
to="/">Realtime GRC Data</router-link>&nbsp;<a class="pull-right navbar-brand"
href="https://dn42.dev/"><img src="/dn42_logo.png" width="173" height="60"/></a>
</div>
</div>
</nav>
<!-- Tabs for content -->
<div class="clearfix">
<app-timer class="float-right"></app-timer>
<b-nav tabs>
<b-nav-item to="flaps" exact exact-active-class="active">Flapping Routes</b-nav-item>
<b-nav-item to="roa" exact exact-active-class="active">ROA Failures</b-nav-item>
</b-nav>
</div>
<!-- content is generated by the router -->
<div class="d-flex justify-content-center mt-3">
<router-view></router-view>
</div>
<!-- footer -->
<footer class="page-footer font-small">
<div style="margin-top: 20px; padding: 5px">
<a href="https://git.burble.com/burble.dn42/dn42grcd">Source Code</a>.
Powered by
<a href="https://getbootstrap.com/">Bootstrap</a>,
<a href="https://vuejs.org">Vue.js</a>,
<a href="https://github.com/axios/axios">axios</a>,
</div>
</footer>
<!-- flap page template -->
<script type="text/x-template" id="app-flaps-template">
<b-container fluid="sm">
<b-row class="justify-content-center py-3">
<h2>Most Updated Routes</h2>
</b-row>
<b-row class="px-3">
<b-col>
<b-table
id="flaps-table"
:fields="fields"
:items="flaps"
:filter="filter"
:sort-by="'count'"
:sort-desc="true"
:per-page="20"
>
<template v-slot:cell(prefix)="data">
<reg-prefix v-bind:prefix="data.value"></reg-prefix>
</template>
<template v-slot:cell(path)="data">
<reg-path v-bind:path="data.value"></reg-path>
</template>
</b-table>
</b-col>
<b-col class="text-left pr-5">
<p>Total Updates: {{ this.total }}</p>
<p><b-pagination class="pb-4"
v-model="currentPage"
:total-rows="rows"
:per-page="20"
aria-controls="flaps-table"
></b-pagination>
</p>
<p><b-input-group size="sm">
<b-form-input
v-model="filter"
type="search"
id="filterInput"
placeholder="Type to filter results"
debounce="200"
></b-form-input>
</b-input-group></p>
</b-col>
</b-row>
</b-container>
</script>
<!-- ROA page template -->
<script type="text/x-template" id="app-roa-template">
<div>
<b-container fluid="sm">
<b-row class="justify-content-center py-3">
<h2>IPv4 ROA Failures</h1>
</b-row>
<b-row class="px-3">
<b-col>
<b-table
:fields="roaFields"
:items="roa4"
:filter="filter4"
>
<template v-slot:cell(prefix)="data">
<reg-prefix v-bind:prefix="data.value"></reg-prefix>
</template>
<template v-slot:cell(origin)="data">
<reg-asn v-bind:asn="data.value"></reg-asn>
</template>
</b-table>
</b-col>
<b-col class="text-left pr-5">
<b-input-group size="sm">
<b-form-input
v-model="filter4"
type="search"
id="filterInput4"
placeholder="Type to filter results"
debounce="200"
></b-form-input>
</b-input-group>
</b-col>
</b-row>
</b-container>
<b-container fluid="sm" class="mw-100">
<b-row class="justify-content-center py-3">
<h2>IPv6 ROA Failures</h1>
</b-row>
<b-row class="px-3">
<b-col>
<b-table
:fields="roaFields"
:items="roa6"
:filter="filter6"
>
<template v-slot:cell(prefix)="data">
<reg-prefix v-bind:prefix="data.value"></reg-prefix>
</template>
<template v-slot:cell(origin)="data">
<reg-asn v-bind:asn="data.value"></reg-asn>
</template>
</b-table>
</b-col>
<b-col class="text-left pr-5">
<b-input-group size="sm">
<b-form-input
v-model="filter6"
type="search"
id="filterInput6"
placeholder="Type to filter results"
debounce="200"
></b-form-input>
</b-input-group>
</b-col>
</b-row>
</b-container>
</div>
</script>
<!-- Element Templates -->
<script type="text/x-template" id="reg-asn">
<b-link v-bind:href="url" v-text="normal"></b-link>
</script>
<script type="text/x-template" id="reg-prefix">
<b-link v-bind:href="url" v-text="prefix"></b-link>
</script>
<script type="text/x-template" id="reg-path">
<ul class="text-nowrap">
<li class="d-inline p-1" v-for="asn in list">
<reg-asn v-bind:asn="asn"></reg-asn>
</li>
</ul>
</script>
<script type="text/x-template" id="app-timer">
<p class="p-1 pr-3">Update in {{ this.seconds }}s</p>
</script>
</div>
<script src="grc.js"></script>
</body>
</html>

284
data.go Normal file
View File

@ -0,0 +1,284 @@
//////////////////////////////////////////////////////////////////////////
// Define data structures
//////////////////////////////////////////////////////////////////////////
package main
//////////////////////////////////////////////////////////////////////////
import (
"context"
"encoding/json"
log "github.com/sirupsen/logrus"
"strings"
"sync/atomic"
"time"
"unsafe"
)
//////////////////////////////////////////////////////////////////////////
// data structures for route updates
type RouteData struct {
paths map[string]uint
}
type RouteInfo struct {
Prefix string `json:"prefix"`
Path string `json:"path"`
Count uint `json:"count"`
}
type RouteSnapshot struct {
cache []byte `json:"-"`
Total uint `json:"total"`
Unique uint `json:"unique"`
List []*RouteInfo `json:"list"`
}
//////////////////////////////////////////////////////////////////////////
// data structures for ROA failures
type ROAData struct {
roas map[string]uint
}
type ROAInfo struct {
Prefix string `json:"prefix"`
Origin string `json:"origin"`
}
type ROASnapshot struct {
cache []byte `json:"-"`
Count uint `json:"count"`
List []*ROAInfo `json:"list"`
}
//////////////////////////////////////////////////////////////////////////
// summary structures
type ActiveData struct {
route *RouteData
roa *ROAData
}
type SnapshotData struct {
route *RouteSnapshot
roa *ROASnapshot
}
type DataStruct struct {
shutdown context.CancelFunc
// sets for currently cellecting and previous data
active *ActiveData
snapshot *SnapshotData
}
//////////////////////////////////////////////////////////////////////////
// initialise the data, and start snapshoting every minute
func StartData() *DataStruct {
data := &DataStruct{}
ctx, cancelFunc := context.WithCancel(context.Background())
data.shutdown = cancelFunc
data.active = data.newActiveData()
data.snapshot = data.active.snapshot()
log.Info("Starting data updates")
go data.swap(ctx)
return data
}
//////////////////////////////////////////////////////////////////////////
func (data *DataStruct) Shutdown() {
data.shutdown()
}
//////////////////////////////////////////////////////////////////////////
// snapshot the live data every 60 seconds
func (data *DataStruct) swap(ctx context.Context) {
// update data at one minute intervals
ticker := time.NewTicker(time.Second * 60)
for {
select {
case <-ctx.Done():
// shutdown
log.Info("Stopping data updates")
return
case <-ticker.C:
// tiemr expired
log.Debug("Data Update")
var upp *unsafe.Pointer
var up unsafe.Pointer
var oldp unsafe.Pointer
// swap in a new empty data set
upp = (*unsafe.Pointer)(unsafe.Pointer(&data.active))
up = unsafe.Pointer(data.newActiveData())
oldp = atomic.SwapPointer(upp, up)
// generate the reporting snapshot
active := (*ActiveData)(oldp)
snapshot := active.snapshot()
// then swap in the new snapshot
upp = (*unsafe.Pointer)(unsafe.Pointer(&data.snapshot))
up = unsafe.Pointer(snapshot)
atomic.SwapPointer(upp, up)
}
}
}
//////////////////////////////////////////////////////////////////////////
// data initialisation
func (data *DataStruct) newActiveData() *ActiveData {
return &ActiveData{
route: data.newRouteData(),
roa: data.newROAData(),
}
}
func (data *DataStruct) newRouteData() *RouteData {
return &RouteData{
paths: make(map[string]uint),
}
}
func (data *DataStruct) newROAData() *ROAData {
return &ROAData{
roas: make(map[string]uint),
}
}
//////////////////////////////////////////////////////////////////////////
// for updates
func (route *RouteData) add(path string) {
route.paths[path] += 1
}
func (roa *ROAData) add(path string) {
roa.roas[path] += 1
}
//////////////////////////////////////////////////////////////////////////
// snapshot the live data
// all data
func (active *ActiveData) snapshot() *SnapshotData {
return &SnapshotData{
route: active.route.snapshot(),
roa: active.roa.snapshot(),
}
}
// route data
func (route *RouteData) snapshot() *RouteSnapshot {
count := uint(len(route.paths))
snapshot := &RouteSnapshot{
Unique: count,
List: make([]*RouteInfo, 0, count),
}
var total uint
// populate the ROA list
for key, value := range route.paths {
space := strings.IndexByte(key, ' ')
if space == -1 {
log.WithFields(log.Fields{
"data": key,
}).Debug("Invalid route data")
} else {
info := &RouteInfo{
Prefix: key[:space],
Path: key[space+1:],
Count: value,
}
total += value
snapshot.List = append(snapshot.List, info)
}
}
snapshot.Total = total
return snapshot
}
// roa data
func (roa *ROAData) snapshot() *ROASnapshot {
count := uint(len(roa.roas))
snapshot := &ROASnapshot{
Count: count,
List: make([]*ROAInfo, 0, count),
}
// populate the ROA list
for key, _ := range roa.roas {
space := strings.IndexByte(key, ' ')
if space == -1 {
log.WithFields(log.Fields{
"data": key,
}).Debug("Invalid roa data")
} else {
info := &ROAInfo{
Prefix: key[:space],
Origin: key[space+1:],
}
snapshot.List = append(snapshot.List, info)
}
}
return snapshot
}
//////////////////////////////////////////////////////////////////////////
// create and cache api responses
func (r *RouteSnapshot) ToJSON() []byte {
if r.cache == nil {
data, err := json.Marshal(r)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("Failed to marshal route json")
} else {
r.cache = data
}
}
return r.cache
}
func (r *ROASnapshot) ToJSON() []byte {
if r.cache == nil {
data, err := json.Marshal(r)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("Failed to marshal ROA json")
} else {
r.cache = data
}
}
return r.cache
}
//////////////////////////////////////////////////////////////////////////
// end of file

91
dn42grcd.go Normal file
View File

@ -0,0 +1,91 @@
//////////////////////////////////////////////////////////////////////////
// DN42 GRC Daemon
//////////////////////////////////////////////////////////////////////////
package main
//////////////////////////////////////////////////////////////////////////
import (
"context"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
"os"
"os/signal"
)
//////////////////////////////////////////////////////////////////////////
// utility function to set the log level
func setLogLevel(levelStr string) {
if level, err := log.ParseLevel(levelStr); err != nil {
// failed to set the level
// set a sensible default and, of course, log the error
log.SetLevel(log.InfoLevel)
log.WithFields(log.Fields{
"loglevel": levelStr,
"error": err,
}).Error("Failed to set requested log level")
} else {
// set the requested level
log.SetLevel(level)
}
}
//////////////////////////////////////////////////////////////////////////
// everything starts here
func main() {
// set a default log level, so that logging can be used immediately
// the level will be overidden later on once the command line
// options are loaded
log.SetLevel(log.InfoLevel)
log.Info("DN42 GRC Daemon Starting")
var (
logLevel = flag.StringP("LogLevel", "l", "Info", "Log level")
socketPath = flag.StringP("SockPath", "p", "bird.sock", "Path to bird fifo")
bindAddress = flag.StringP("BindAddress", "b", "[::]:8050", "Server bind address")
staticRoot = flag.StringP("StaticRoot", "s", "StaticRoot", "Static page directory")
)
flag.Parse()
// now initialise logging properly based on the cmd line options
setLogLevel(*logLevel)
// start the API server and begin ingesting data
data := StartData()
server := StartAPIServer(*bindAddress, data, *staticRoot)
ingest := StartIngesting(*socketPath, data)
// graceful shutdown via SIGINT (^C)
csig := make(chan os.Signal, 1)
signal.Notify(csig, os.Interrupt)
// and block
<-csig
log.Info("Server shutting down")
// deadline for server to shutdown
ctx, cancel := context.WithTimeout(context.Background(), 10)
defer cancel()
// shutdown the server and stop ingesting data
ingest.Shutdown()
server.Shutdown(ctx)
data.Shutdown()
// nothing left to do
log.Info("Shutdown complete, all done")
os.Exit(0)
}
//////////////////////////////////////////////////////////////////////////
// end of file

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module burble.dn42/dn42grcd
go 1.15
require (
github.com/gorilla/mux v1.8.0
github.com/sirupsen/logrus v1.7.0
github.com/spf13/pflag v1.0.5
)

14
go.sum Normal file
View File

@ -0,0 +1,14 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

157
ingest.go Normal file
View File

@ -0,0 +1,157 @@
//////////////////////////////////////////////////////////////////////////
// Ingest updates from the bird socket to the internal data structures
//////////////////////////////////////////////////////////////////////////
package main
//////////////////////////////////////////////////////////////////////////
import (
"bufio"
"context"
log "github.com/sirupsen/logrus"
"os"
"strings"
"time"
)
//////////////////////////////////////////////////////////////////////////
type Ingest struct {
shutdown context.CancelFunc
socket *os.File
data *DataStruct
}
//////////////////////////////////////////////////////////////////////////
// initialise and start collecting data
func StartIngesting(sockPath string, data *DataStruct) *Ingest {
ingest := &Ingest{data: data}
// create cancellation context
ctx, cancelFunc := context.WithCancel(context.Background())
ingest.shutdown = cancelFunc
// start reading from the socket
go ingest.tail(ctx, sockPath)
return ingest
}
//////////////////////////////////////////////////////////////////////////
func (ingest *Ingest) Shutdown() {
// signal that the tail function should close
ingest.shutdown()
// and close the file to ensure it gets kicked out the read loop
ingest.socket.Close()
}
//////////////////////////////////////////////////////////////////////////
// continuously read lines from the socket until closed
// the function will re-open the socket on failures
func (ingest *Ingest) tail(ctx context.Context, sockPath string) {
for {
// have I been cancelled ?
select {
case <-ctx.Done():
log.Debug("Ingestion tail cancelled")
break
default:
}
// open the named pipe
sock, err := os.OpenFile(sockPath, os.O_RDONLY, os.ModeNamedPipe)
if err != nil {
// if an error occured, sleep for a bit and retry
log.WithFields(log.Fields{
"path": sockPath,
}).Fatal("Failed to open fifo, pausing")
time.Sleep(time.Second * 5)
} else {
ingest.socket = sock
log.WithFields(log.Fields{
"path": sockPath,
}).Info("Opened bird socket")
reader := bufio.NewReader(sock)
// read from the socket
for {
line, err := reader.ReadString('\n')
if err != nil {
// error occurred, break out the read loop
log.WithFields(log.Fields{
"error": err,
}).Warn("Error reading from socket")
break
}
// trim the trailing newline
line = strings.TrimSuffix(line, "\n")
if len(line) > 3 {
log.WithFields(log.Fields{
"line": line,
}).Debug("Received line")
// action based on first two characters
action := line[:2]
// data is from char 3 because of the space
data := line[3:]
switch action {
case "!1":
ingest.route(data)
case "!2":
ingest.roaFail(data)
default:
// ignore anything else
}
}
}
// ensure socket is closed
ingest.socket.Close()
}
}
}
//////////////////////////////////////////////////////////////////////////
// ingest a route update
func (ingest *Ingest) route(data string) {
log.WithFields(log.Fields{
"data": data,
}).Debug("route update")
ingest.data.active.route.add(data)
}
//////////////////////////////////////////////////////////////////////////
// ingest an ROA Fail
func (ingest *Ingest) roaFail(data string) {
// update the datastructures
log.WithFields(log.Fields{
"data": data,
}).Debug("roa fail")
ingest.data.active.roa.add(data)
}
//////////////////////////////////////////////////////////////////////////
// end of file

147
server.go Normal file
View File

@ -0,0 +1,147 @@
//////////////////////////////////////////////////////////////////////////
// API to serve the internal data structures
//////////////////////////////////////////////////////////////////////////
package main
//////////////////////////////////////////////////////////////////////////
import (
"context"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"net/http"
"os"
"time"
)
//////////////////////////////////////////////////////////////////////////
type APIServer struct {
data *DataStruct
server *http.Server
router *mux.Router
}
//////////////////////////////////////////////////////////////////////////
// http request logger
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.WithFields(log.Fields{
"method": r.Method,
"URL": r.URL.String(),
"Remote": r.RemoteAddr,
}).Debug("HTTP Request")
next.ServeHTTP(w, r)
})
}
//////////////////////////////////////////////////////////////////////////
// start the API servers
func StartAPIServer(bindAddress string, data *DataStruct,
staticRoot string) *APIServer {
api := &APIServer{
data: data,
router: mux.NewRouter(),
}
// initialise http server
api.server = &http.Server{
Addr: bindAddress,
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
Handler: api.router,
}
// add the api
api.router.Use(requestLogger)
s := api.router.Methods("GET").PathPrefix("/api").Subrouter()
s.HandleFunc("/flaps", api.handleFlaps)
s.HandleFunc("/roa", api.handleROA)
api.installStaticRoutes(staticRoot)
// run the server in a non-blocking goroutine
log.WithFields(log.Fields{
"BindAddress": bindAddress,
}).Info("Starting server")
go func() {
if err := api.server.ListenAndServe(); err != nil {
log.WithFields(log.Fields{
"error": err,
"BindAddress": bindAddress,
}).Fatal("Unable to start server")
}
}()
return api
}
//////////////////////////////////////////////////////////////////////////
// shutdown the API Server
func (api *APIServer) Shutdown(ctx context.Context) {
api.server.Shutdown(ctx)
}
//////////////////////////////////////////////////////////////////////////
// handler funcs
func (api *APIServer) handleFlaps(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write(api.data.snapshot.route.ToJSON())
}
func (api *APIServer) handleROA(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write(api.data.snapshot.roa.ToJSON())
}
//////////////////////////////////////////////////////////////////////////
// static route handler
func (api *APIServer) installStaticRoutes(staticPath string) {
// an empty path disables static route serving
if staticPath == "" {
log.Info("Disabling static route serving")
return
}
// validate that the staticPath exists
stat, err := os.Stat(staticPath)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"path": staticPath,
}).Fatal("Unable to find static page directory")
}
// and it is a directory
if !stat.IsDir() {
log.WithFields(log.Fields{
"error": err,
"path": staticPath,
}).Fatal("Static path is not a directory")
}
// install a file server for the static route
api.router.PathPrefix("/").Handler(http.StripPrefix("/",
http.FileServer(http.Dir(staticPath)))).Methods("GET")
log.WithFields(log.Fields{
"path": staticPath,
}).Info("Static route installed")
}
//////////////////////////////////////////////////////////////////////////
// end of file