frontend: add API
This commit is contained in:
212
README.md
212
README.md
@@ -1,12 +1,34 @@
|
||||
Bird-lg-go
|
||||
==========
|
||||
# Bird-lg-go
|
||||
|
||||
An alternative implementation for [bird-lg](https://github.com/sileht/bird-lg) written in Go. Both frontend and backend (proxy) are implemented, and can work with either the original Python implementation or the Go implementation.
|
||||
|
||||
> The code on master branch no longer support BIRDv1. Branch "bird1" is the last version that supports BIRDv1.
|
||||
|
||||
Frontend
|
||||
--------
|
||||
## Table of Contents
|
||||
|
||||
* [Bird-lg-go](#bird-lg-go)
|
||||
* [Table of Contents](#table-of-contents)
|
||||
* [Frontend](#frontend)
|
||||
* [Proxy](#proxy)
|
||||
* [Advanced Features](#advanced-features)
|
||||
* [API](#api)
|
||||
* [Request fields](#request-fields)
|
||||
* [Response fields (when type is summary)](#response-fields-when-type-is-summary)
|
||||
* [Fields for apiSummaryResultPair](#fields-for-apisummaryresultpair)
|
||||
* [Fields for SummaryRowData](#fields-for-summaryrowdata)
|
||||
* [Example response](#example-response)
|
||||
* [Response fields (when type is bird, traceroute or whois)](#response-fields-when-type-is-bird-traceroute-or-whois)
|
||||
* [Fields for apiGenericResultPair](#fields-for-apigenericresultpair)
|
||||
* [Example response](#example-response-1)
|
||||
* [Telegram Bot Webhook](#telegram-bot-webhook)
|
||||
* [Example of setting the webhook](#example-of-setting-the-webhook)
|
||||
* [Supported commands](#supported-commands)
|
||||
* [Credits](#credits)
|
||||
* [License](#license)
|
||||
|
||||
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
|
||||
|
||||
## Frontend
|
||||
|
||||
The frontend directory contains the code for the web frontend, where users see BGP states, do traceroutes and whois, etc. It's a replacement for "lg.py" in original bird-lg project.
|
||||
|
||||
@@ -50,8 +72,7 @@ Example: the following docker-compose.yml entry does the same as above, but by s
|
||||
|
||||
Demo: https://lg.lantian.pub
|
||||
|
||||
Proxy
|
||||
-----
|
||||
## Proxy
|
||||
|
||||
The proxy directory contains the code for the "proxy" for bird commands and traceroutes. It's a replacement for "lgproxy.py" in original bird-lg project.
|
||||
|
||||
@@ -91,14 +112,185 @@ Example: the following docker-compose.yml entry does the same as above, but by s
|
||||
|
||||
You can use source IP restriction to increase security. You should also bind the proxy to a specific interface and use an external firewall/iptables for added security.
|
||||
|
||||
Credits
|
||||
-------
|
||||
## Advanced Features
|
||||
|
||||
### API
|
||||
|
||||
The frontend provides an API for running BIRD/traceroute/whois queries.
|
||||
|
||||
API Endpoint: `https://your.frontend.com:5000/api/` (the last slash must not be omitted!)
|
||||
|
||||
Requests are sent as POSTS with JSON bodies.
|
||||
|
||||
#### Request fields
|
||||
|
||||
| Name | Type | Value |
|
||||
| ---- | ---- | -------- |
|
||||
| `servers` | `[]string` | List of servers to be queried |
|
||||
| `type` | `string` | Can be `summary`, `bird`, `traceroute` or `whois` |
|
||||
| `args` | `string` | Arguments to be passed, see below |
|
||||
|
||||
Argument examples for each type:
|
||||
|
||||
- `summary`: `args` is ignored. Recommended to set to empty string.
|
||||
- `bird`: `args` is the command to be passed to bird, e.g. `show route for 8.8.8.8`
|
||||
- `traceroute`: `args` is the traceroute target, e.g. `8.8.8.8` or `google.com`
|
||||
- `whois`: `args` is the whois target, e.g. `8.8.8.8` or `google.com`
|
||||
|
||||
Example request:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
"alpha"
|
||||
],
|
||||
"type": "bird",
|
||||
"args": "show route for 8.8.8.8"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response fields (when `type` is `summary`)
|
||||
|
||||
| Name | Type | Value |
|
||||
| ---- | ---- | -------- |
|
||||
| `error` | `string` | Error message when something is wrong. Empty when everything good |
|
||||
| `result` | array of `apiSummaryResultPair` | See below |
|
||||
|
||||
##### Fields for `apiSummaryResultPair`
|
||||
|
||||
| Name | Type | Value |
|
||||
| ---- | ---- | -------- |
|
||||
| `server` | `string` | Name of the server |
|
||||
| `data` | array of `SummaryRowData` | Summaries of the server, see below |
|
||||
|
||||
##### Fields for `SummaryRowData`
|
||||
|
||||
All fields below is 1:1 correspondent to the output of `birdc show protocols`.
|
||||
|
||||
| Name | Type |
|
||||
| ---- | ---- |
|
||||
| `name` | `string` |
|
||||
| `proto` | `string` |
|
||||
| `table` | `string` |
|
||||
| `state` | `string` |
|
||||
| `since` | `string` |
|
||||
| `info` | `string` |
|
||||
|
||||
##### Example response
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
"alpha"
|
||||
],
|
||||
"type": "summary",
|
||||
"args": ""
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "",
|
||||
"result": [
|
||||
{
|
||||
"server": "alpha",
|
||||
"data": [
|
||||
{
|
||||
"name": "bgp1",
|
||||
"proto": "BGP",
|
||||
"table": "---",
|
||||
"state": "start",
|
||||
"since": "2021-01-15 22:40:01",
|
||||
"info": "Active Socket: Operation timed out"
|
||||
},
|
||||
{
|
||||
"name": "bgp2",
|
||||
"proto": "BGP",
|
||||
"table": "---",
|
||||
"state": "start",
|
||||
"since": "2021-01-03 08:15:48",
|
||||
"info": "Established"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Response fields (when `type` is `bird`, `traceroute` or `whois`)
|
||||
|
||||
| Name | Type | Value |
|
||||
| ---- | ---- | -------- |
|
||||
| `error` | `string` | Error message, empty when everything is good |
|
||||
| `result` | array of `apiGenericResultPair` | See below |
|
||||
|
||||
##### Fields for `apiGenericResultPair`
|
||||
|
||||
| Name | Type | Value |
|
||||
| ---- | ---- | -------- |
|
||||
| `server` | `string` | Name of the server; is empty when type is `whois` |
|
||||
| `data` | `string` | Result from the server |
|
||||
|
||||
##### Example response
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
"alpha"
|
||||
],
|
||||
"type": "bird",
|
||||
"args": "show status"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "",
|
||||
"result": [
|
||||
{
|
||||
"server": "alpha",
|
||||
"data": "BIRD v2.0.7-137-g61dae32b\nRouter ID is 1.2.3.4\nCurrent server time is 2021-01-17 04:21:14.792\nLast reboot on 2021-01-03 08:15:48.494\nLast reconfiguration on 2021-01-17 00:49:10.573\nDaemon is up and running\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Telegram Bot Webhook
|
||||
|
||||
The frontend can act as a Telegram Bot webhook endpoint, to add BGP route/traceroute/whois lookup functionality to your tech group.
|
||||
|
||||
There is no configuration necessary on the frontend, just start it up normally.
|
||||
|
||||
Set your Telegram Bot webhook URL to `https://your.frontend.com:5000/telegram/alpha+beta+gamma`, where `alpha+beta+gamma` is the list of servers to be queried on Telegram commands, separated by `+`.
|
||||
|
||||
You may omit `alpha+beta+gamma` to use all your servers, but it is not recommended when you have lots of servers, or the message would be too long and hard to read.
|
||||
|
||||
#### Example of setting the webhook
|
||||
|
||||
```bash
|
||||
curl "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook?url=https://your.frontend.com:5000/telegram/alpha+beta+gamma"
|
||||
```
|
||||
|
||||
#### Supported commands
|
||||
|
||||
- `path`: Show bird's ASN path to target IP
|
||||
- `route`: Show bird's preferred route to target IP
|
||||
- `trace`: Traceroute to target IP/domain
|
||||
- `whois`: Whois query
|
||||
|
||||
## Credits
|
||||
|
||||
- Everyone who contributed to this project (see Contributors section on the right)
|
||||
- Mehdi Abaakouk for creating [the original bird-lg project](https://github.com/sileht/bird-lg)
|
||||
- [Bootstrap](https://getbootstrap.com/) as web UI framework
|
||||
|
||||
License
|
||||
-------
|
||||
## License
|
||||
|
||||
GPL 3.0
|
||||
|
||||
116
frontend/api.go
Normal file
116
frontend/api.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type apiRequest struct {
|
||||
Servers []string `json:"servers"`
|
||||
Type string `json:"type"`
|
||||
Args string `json:"args"`
|
||||
}
|
||||
|
||||
type apiGenericResultPair struct {
|
||||
Server string `json:"server"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type apiSummaryResultPair struct {
|
||||
Server string `json:"server"`
|
||||
Data []SummaryRowData `json:"data"`
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
Error string `json:"error"`
|
||||
Result []interface{} `json:"result"`
|
||||
}
|
||||
|
||||
var apiHandlerMap = map[string](func(request apiRequest) apiResponse){
|
||||
"summary": apiSummaryHandler,
|
||||
"bird": apiGenericHandlerFactory("bird"),
|
||||
"traceroute": apiGenericHandlerFactory("traceroute"),
|
||||
"whois": apiWhoisHandler,
|
||||
}
|
||||
|
||||
func apiGenericHandlerFactory(endpoint string) func(request apiRequest) apiResponse {
|
||||
return func(request apiRequest) apiResponse {
|
||||
results := batchRequest(request.Servers, endpoint, request.Args)
|
||||
var response apiResponse
|
||||
|
||||
for i, result := range results {
|
||||
response.Result = append(response.Result, &apiGenericResultPair{
|
||||
Server: request.Servers[i],
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
func apiSummaryHandler(request apiRequest) apiResponse {
|
||||
results := batchRequest(request.Servers, "bird", "show protocols")
|
||||
var response apiResponse
|
||||
|
||||
for i, result := range results {
|
||||
parsedSummary, err := summaryParse(result, request.Servers[i])
|
||||
if err != nil {
|
||||
return apiResponse{
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
response.Result = append(response.Result, &apiSummaryResultPair{
|
||||
Server: request.Servers[i],
|
||||
Data: parsedSummary.Rows,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func apiWhoisHandler(request apiRequest) apiResponse {
|
||||
return apiResponse{
|
||||
Error: "",
|
||||
Result: []interface{}{
|
||||
apiGenericResultPair{
|
||||
Server: "",
|
||||
Data: whois(request.Args),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func apiErrorHandler(err error) apiResponse {
|
||||
return apiResponse{
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request apiRequest
|
||||
var response apiResponse
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
||||
response = apiResponse{
|
||||
Error: err.Error(),
|
||||
}
|
||||
} else {
|
||||
handler := apiHandlerMap[request.Type]
|
||||
if handler == nil {
|
||||
response = apiErrorHandler(errors.New("Invalid request type"))
|
||||
} else {
|
||||
response = handler(request)
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
bytes, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return
|
||||
}
|
||||
w.Write(bytes)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -99,18 +100,17 @@ func smartFormatter(s string) string {
|
||||
return result
|
||||
}
|
||||
|
||||
// Output a table for the summary page
|
||||
func summaryTable(data string, serverName string) string {
|
||||
// Parse bird show protocols result
|
||||
func summaryParse(data string, serverName string) (TemplateSummary, error) {
|
||||
args := TemplateSummary{
|
||||
ServerName: serverName,
|
||||
Raw: data,
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(data), "\n")
|
||||
if len(lines) <= 1 {
|
||||
// Likely backend returned an error message
|
||||
return "<pre>" + template.HTMLEscapeString(strings.TrimSpace(data)) + "</pre>"
|
||||
}
|
||||
|
||||
args := TemplateSummary{
|
||||
ServerName: serverName,
|
||||
Raw: data,
|
||||
return args, errors.New(strings.TrimSpace(data))
|
||||
}
|
||||
|
||||
// extract the table header
|
||||
@@ -167,10 +167,21 @@ func summaryTable(data string, serverName string) string {
|
||||
args.Rows = append(args.Rows, row)
|
||||
}
|
||||
|
||||
// finally, render the summary template
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Output a table for the summary page
|
||||
func summaryTable(data string, serverName string) string {
|
||||
result, err := summaryParse(data, serverName)
|
||||
|
||||
if err != nil {
|
||||
return "<pre>" + template.HTMLEscapeString(err.Error()) + "</pre>"
|
||||
}
|
||||
|
||||
// render the summary template
|
||||
tmpl := TemplateLibrary["summary"]
|
||||
var buffer bytes.Buffer
|
||||
err := tmpl.Execute(&buffer, args)
|
||||
err = tmpl.Execute(&buffer, result)
|
||||
if err != nil {
|
||||
fmt.Println("Error rendering summary:", err.Error())
|
||||
}
|
||||
|
||||
@@ -32,15 +32,14 @@ type TemplatePage struct {
|
||||
}
|
||||
|
||||
// summary
|
||||
|
||||
type SummaryRowData struct {
|
||||
Name string
|
||||
Proto string
|
||||
Table string
|
||||
State string
|
||||
MappedState string
|
||||
Since string
|
||||
Info string
|
||||
Name string `json:"name"`
|
||||
Proto string `json:"proto"`
|
||||
Table string `json:"table"`
|
||||
State string `json:"state"`
|
||||
MappedState string `json:"-"`
|
||||
Since string `json:"since"`
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
// utility functions to allow filtering of results in the template
|
||||
|
||||
@@ -210,6 +210,7 @@ func webServerStart() {
|
||||
http.HandleFunc("/generic/", webBackendCommunicator("bird", "generic"))
|
||||
http.HandleFunc("/traceroute/", webBackendCommunicator("traceroute", "traceroute"))
|
||||
http.HandleFunc("/whois/", webHandlerWhois)
|
||||
http.HandleFunc("/api/", apiHandler)
|
||||
http.HandleFunc("/telegram/", webHandlerTelegramBot)
|
||||
|
||||
// Start HTTP server
|
||||
|
||||
Reference in New Issue
Block a user