You've already forked bird-lg-go
mirror of
https://github.com/xddxdd/bird-lg-go
synced 2025-10-17 22:42:12 +02:00
Compare commits
51 Commits
bird1
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fa827502cf | ||
![]() |
794125a96f | ||
![]() |
de9d9101b1 | ||
![]() |
056ef3769e | ||
![]() |
974e809deb | ||
![]() |
6e19b5ae64 | ||
![]() |
874089117b | ||
![]() |
f77a8a28fe | ||
![]() |
9e8a845658 | ||
![]() |
fd3e7b8379 | ||
![]() |
8765189deb | ||
![]() |
492942cce1 | ||
![]() |
f81a5308ae | ||
![]() |
5b5a09ccbd | ||
![]() |
dc4d7e6532 | ||
![]() |
28a7d2a53f | ||
![]() |
4413f1032f | ||
![]() |
007b66e036 | ||
![]() |
3f612d2e76 | ||
![]() |
f49f8bac5e | ||
![]() |
f6ddc5761b | ||
![]() |
1c3d9ec594 | ||
![]() |
6cc0c617b4 | ||
![]() |
e2cc580da3 | ||
![]() |
472cec74b0 | ||
![]() |
da2c3d9aed | ||
![]() |
aa76bc3de7 | ||
![]() |
a984095282 | ||
![]() |
1baf325149 | ||
![]() |
72946e1113 | ||
![]() |
90e5012840 | ||
![]() |
8d5eb56199 | ||
![]() |
8d0618fed9 | ||
![]() |
f8ea511d44 | ||
![]() |
b99eb60c30 | ||
![]() |
9f934ca53c | ||
![]() |
ee7cc1675b | ||
![]() |
f4b6955343 | ||
![]() |
78ce724171 | ||
![]() |
6179c688be | ||
![]() |
8d0e210572 | ||
![]() |
26c51176e4 | ||
![]() |
5cf2ac57b8 | ||
![]() |
75bc63ffa7 | ||
![]() |
438c6a1f82 | ||
![]() |
b98d783739 | ||
![]() |
5000ad1bbf | ||
![]() |
538699ccd2 | ||
![]() |
9e77de6b46 | ||
![]() |
c15942cc32 | ||
![]() |
3bcfc3d36c |
61
.circleci/config.yml
Normal file
61
.circleci/config.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
version: 2.1
|
||||
|
||||
workflows:
|
||||
docker:
|
||||
jobs:
|
||||
- build
|
||||
- deploy:
|
||||
context:
|
||||
- docker
|
||||
requires:
|
||||
- build
|
||||
matrix:
|
||||
parameters:
|
||||
program: [frontend, proxy]
|
||||
# latest is amd64 arch + push to default latest tag
|
||||
image_arch: [latest, i386, arm32v7, arm64v8, ppc64le, s390x]
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.15
|
||||
working_directory: /go/src/github.com/xddxdd/bird-lg-go
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -v -t -d ./...
|
||||
- run: go get -u github.com/kevinburke/go-bindata/...
|
||||
- run: cd frontend && go generate
|
||||
- run: go test -v ./...
|
||||
deploy:
|
||||
docker:
|
||||
- image: circleci/golang:1.15
|
||||
working_directory: /go/src/github.com/xddxdd/bird-lg-go
|
||||
parameters:
|
||||
image_arch:
|
||||
type: string
|
||||
program:
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 19.03.13
|
||||
- run:
|
||||
name: Install GPP
|
||||
command: |
|
||||
sudo apt-get update && sudo apt-get install -y gpp
|
||||
- run:
|
||||
name: Build Docker image
|
||||
environment:
|
||||
IMAGE_ARCH: << parameters.image_arch >>
|
||||
PROGRAM: << parameters.program >>
|
||||
BUILD_ID: << pipeline.number >>
|
||||
command: |
|
||||
make -f Makefile.docker _crossbuild
|
||||
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
|
||||
make -f Makefile.docker \
|
||||
DOCKER_USERNAME=$DOCKER_USERNAME \
|
||||
BUILD_ID=circleci-build$BUILD_ID \
|
||||
$PROGRAM/$IMAGE_ARCH
|
16
.github/dependabot.yml
vendored
Normal file
16
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "08:00"
|
||||
timezone: Asia/Shanghai
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: gomod
|
||||
directory: "/proxy"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "08:00"
|
||||
timezone: Asia/Shanghai
|
||||
open-pull-requests-limit: 10
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -16,4 +16,11 @@
|
||||
|
||||
.DS_Store
|
||||
frontend/frontend
|
||||
proxy/proxy
|
||||
proxy/proxy
|
||||
|
||||
# don't include generated bindata file
|
||||
frontend/bindata.go
|
||||
|
||||
# don't include generated Dockerfiles
|
||||
frontend/Dockerfile.*
|
||||
proxy/Dockerfile.*
|
35
.travis.yml
35
.travis.yml
@@ -1,35 +0,0 @@
|
||||
language: minimal
|
||||
os: linux
|
||||
dist: focal
|
||||
services:
|
||||
- docker
|
||||
env:
|
||||
- PROGRAM=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=i386
|
||||
- PROGRAM=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=amd64
|
||||
- PROGRAM=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=arm32v7
|
||||
- PROGRAM=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=arm64v8
|
||||
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=i386
|
||||
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=amd64
|
||||
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=arm32v7
|
||||
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=arm64v8
|
||||
|
||||
install:
|
||||
- docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
|
||||
script:
|
||||
- |
|
||||
# Build image
|
||||
docker build \
|
||||
-t $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH \
|
||||
-f $PROGRAM/Dockerfile.$IMAGE_ARCH \
|
||||
$PROGRAM
|
||||
|
||||
# Tag image :{arch} and :{arch}-build{build number}
|
||||
docker tag $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH-build$TRAVIS_BUILD_NUMBER
|
||||
if [ "$IMAGE_ARCH" = "amd64" ]; then
|
||||
# Tag as latest for amd64 images
|
||||
docker tag $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH $DOCKER_USERNAME/$IMAGE_NAME:latest
|
||||
docker tag $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH $DOCKER_USERNAME/$IMAGE_NAME:build$TRAVIS_BUILD_NUMBER
|
||||
fi
|
||||
- docker push $DOCKER_USERNAME/$IMAGE_NAME
|
202
API.md
Normal file
202
API.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Bird-lg-go API documentation
|
||||
|
||||
The frontend provides an API for running BIRD/traceroute/whois queries.
|
||||
|
||||
API Endpoint: `https://your.frontend.com/api/` (the last slash must not be omitted!)
|
||||
|
||||
Requests are sent as POSTS with JSON bodies.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [Bird-lg-go API documentation](#bird-lg-go-api-documentation)
|
||||
* [Table of Contents](#table-of-contents)
|
||||
* [Request fields](#request-fields)
|
||||
* [Example request of type bird](#example-request-of-type-bird)
|
||||
* [Example request of type server_list](#example-request-of-type-server_list)
|
||||
* [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, whois or server_list)](#response-fields-when-type-is-bird-traceroute-whois-or-server_list)
|
||||
* [Fields for apiGenericResultPair](#fields-for-apigenericresultpair)
|
||||
* [Example response of type bird](#example-response-of-type-bird)
|
||||
* [Example response of type server_list](#example-response-of-type-server_list)
|
||||
|
||||
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
|
||||
|
||||
## Request fields
|
||||
|
||||
| Name | Type | Value |
|
||||
| ---- | ---- | -------- |
|
||||
| `servers` | array of `string` | List of servers to be queried |
|
||||
| `type` | `string` | Can be `summary`, `bird`, `traceroute`, `whois` or `server_list` |
|
||||
| `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`
|
||||
- `server_list`: `args` is ignored. In addition, `servers` is also ignored.
|
||||
|
||||
### Example request of type `bird`
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
"alpha"
|
||||
],
|
||||
"type": "bird",
|
||||
"args": "show route for 8.8.8.8"
|
||||
}
|
||||
```
|
||||
|
||||
### Example request of type `server_list`
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [],
|
||||
"type": "server_list",
|
||||
"args": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Response fields (when `type` is `summary`)
|
||||
|
||||
| Name | Type | Value |
|
||||
| ---- | ---- | -------- |
|
||||
| `error` | `string` | Error message when something is wrong. Empty when everything is 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`, `whois` or `server_list`)
|
||||
|
||||
| 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; is empty when type is `server_list` |
|
||||
|
||||
### Example response of type `bird`
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [],
|
||||
"type": "server_list",
|
||||
"args": ""
|
||||
}
|
||||
```
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Example response of type `server_list`
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
"alpha"
|
||||
],
|
||||
"type": "bird",
|
||||
"args": "show status"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "",
|
||||
"result": [
|
||||
{
|
||||
"server": "gigsgigscloud",
|
||||
"data": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
13
Makefile
Normal file
13
Makefile
Normal file
@@ -0,0 +1,13 @@
|
||||
frontend:
|
||||
$(MAKE) -C frontend all
|
||||
|
||||
proxy:
|
||||
$(MAKE) -C proxy all
|
||||
|
||||
.DEFAULT_GOAL := all
|
||||
.PHONY: all frontend proxy
|
||||
all: frontend proxy
|
||||
|
||||
install:
|
||||
install -m 755 frontend/frontend /usr/local/bin/frontend
|
||||
install -m 755 proxy/proxy /usr/local/bin/proxy
|
77
Makefile.docker
Normal file
77
Makefile.docker
Normal file
@@ -0,0 +1,77 @@
|
||||
# Basic definitions
|
||||
DOCKER_USERNAME := xddxdd
|
||||
ARCHITECTURES := amd64 i386 arm32v7 arm64v8 ppc64le s390x
|
||||
IMAGES := frontend proxy
|
||||
|
||||
# General Purpose Preprocessor config
|
||||
GPP_INCLUDE_DIR := include
|
||||
GPP_FLAGS_U := "" "" "(" "," ")" "(" ")" "\#" ""
|
||||
GPP_FLAGS_M := "\#" "\n" " " " " "\n" "(" ")"
|
||||
GPP_FLAGS_EXTRA := +c "\\\n" ""
|
||||
GPP_FLAGS := -I ${GPP_INCLUDE_DIR} --nostdinc -U ${GPP_FLAGS_U} -M ${GPP_FLAGS_M} ${GPP_FLAGS_EXTRA}
|
||||
|
||||
BUILD_ID ?= $(shell date +%Y%m%d%H%M)
|
||||
|
||||
define create-image-arch-target
|
||||
frontend/Dockerfile.$1: frontend/template.Dockerfile
|
||||
@gpp ${GPP_FLAGS} -D ARCH_$(shell echo $1 | tr a-z A-Z) -o frontend/Dockerfile.$1 frontend/template.Dockerfile || rm -rf frontend/Dockerfile.$1
|
||||
|
||||
frontend/$1: frontend/Dockerfile.$1
|
||||
@if [ -f frontend/Dockerfile.$1 ]; then \
|
||||
docker build --pull --no-cache -t ${DOCKER_USERNAME}/bird-lg-go:$1-${BUILD_ID} -f frontend/Dockerfile.$1 frontend || exit 1; \
|
||||
docker push ${DOCKER_USERNAME}/bird-lg-go:$1-${BUILD_ID} || exit 1; \
|
||||
docker tag ${DOCKER_USERNAME}/bird-lg-go:$1-${BUILD_ID} ${DOCKER_USERNAME}/bird-lg-go:$1 || exit 1; \
|
||||
docker push ${DOCKER_USERNAME}/bird-lg-go:$1 || exit 1; \
|
||||
else \
|
||||
echo "Dockerfile generation failed, see error above"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
proxy/Dockerfile.$1: proxy/template.Dockerfile
|
||||
@gpp ${GPP_FLAGS} -D ARCH_$(shell echo $1 | tr a-z A-Z) -o proxy/Dockerfile.$1 proxy/template.Dockerfile || rm -rf proxy/Dockerfile.$1
|
||||
|
||||
proxy/$1: proxy/Dockerfile.$1
|
||||
@if [ -f proxy/Dockerfile.$1 ]; then \
|
||||
docker build --pull --no-cache -t ${DOCKER_USERNAME}/bird-lgproxy-go:$1-${BUILD_ID} -f proxy/Dockerfile.$1 proxy || exit 1; \
|
||||
docker push ${DOCKER_USERNAME}/bird-lgproxy-go:$1-${BUILD_ID} || exit 1; \
|
||||
docker tag ${DOCKER_USERNAME}/bird-lgproxy-go:$1-${BUILD_ID} ${DOCKER_USERNAME}/bird-lgproxy-go:$1 || exit 1; \
|
||||
docker push ${DOCKER_USERNAME}/bird-lgproxy-go:$1 || exit 1; \
|
||||
else \
|
||||
echo "Dockerfile generation failed, see error above"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
endef
|
||||
|
||||
$(foreach arch,${ARCHITECTURES},$(eval $(call create-image-arch-target,$(arch))))
|
||||
|
||||
frontend:$(foreach arch,latest ${ARCHITECTURES},frontend/${arch})
|
||||
|
||||
frontend/latest: frontend/amd64
|
||||
@docker tag ${DOCKER_USERNAME}/bird-lg-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lg-go:${BUILD_ID} || exit 1
|
||||
@docker push ${DOCKER_USERNAME}/bird-lg-go:${BUILD_ID} || exit 1
|
||||
@docker tag ${DOCKER_USERNAME}/bird-lg-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lg-go:latest || exit 1
|
||||
@docker push ${DOCKER_USERNAME}/bird-lg-go:latest || exit 1
|
||||
|
||||
proxy:$(foreach arch,latest ${ARCHITECTURES},proxy/${arch})
|
||||
|
||||
proxy/latest: proxy/amd64
|
||||
@docker tag ${DOCKER_USERNAME}/bird-lgproxy-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lgproxy-go:${BUILD_ID} || exit 1
|
||||
@docker push ${DOCKER_USERNAME}/bird-lgproxy-go:${BUILD_ID} || exit 1
|
||||
@docker tag ${DOCKER_USERNAME}/bird-lgproxy-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lgproxy-go:latest || exit 1
|
||||
@docker push ${DOCKER_USERNAME}/bird-lgproxy-go:latest || exit 1
|
||||
|
||||
.DEFAULT_GOAL := images
|
||||
.DELETE_ON_ERROR:
|
||||
.SECONDARY:
|
||||
|
||||
# Target to enable multiarch support
|
||||
_crossbuild:
|
||||
@docker run --rm --privileged multiarch/qemu-user-static --reset -p yes >/dev/null
|
||||
|
||||
dockerfiles: $(foreach image,${IMAGES},$(foreach arch,${ARCHITECTURES},$(image)/Dockerfile.$(arch)))
|
||||
|
||||
images: $(foreach image,${IMAGES},$(image))
|
||||
|
||||
clean:
|
||||
@rm -rf */Dockerfile.{$(shell echo ${ARCHITECTURES} | sed "s/ /,/g")}
|
138
README.md
138
README.md
@@ -1,10 +1,50 @@
|
||||
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.
|
||||
|
||||
Frontend
|
||||
--------
|
||||
> The code on master branch no longer support BIRDv1. Branch "bird1" is the last version that supports BIRDv1.
|
||||
|
||||
## 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)
|
||||
* [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)
|
||||
|
||||
## Build Instructions
|
||||
|
||||
Run `make` to build binaries for both the frontend and the proxy. You need to have Go installed on your machine.
|
||||
|
||||
Optionally run `make install` to install them to `/usr/local/bin`.
|
||||
|
||||
Or, you can manually do the building steps:
|
||||
|
||||
```bash
|
||||
# Build frontend binary
|
||||
cd frontend
|
||||
go get -u github.com/kevinburke/go-bindata/...
|
||||
go generate
|
||||
go build -ldflags "-w -s" -o frontend
|
||||
cd ..
|
||||
|
||||
# Build proxy binary
|
||||
cd proxy
|
||||
go build -ldflags "-w -s" -o proxy
|
||||
cd ..
|
||||
```
|
||||
|
||||
- If you get `undefined: MustAssetString`, you need to uninstall an older version of go-bindata from your machine: see [#11](https://github.com/xddxdd/bird-lg-go/issues/11)
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -18,11 +58,16 @@ Features implemented:
|
||||
|
||||
Usage: all configuration is done via commandline parameters or environment variables, no config file.
|
||||
|
||||
- --servers / BIRDLG_SERVERS: server name prefixes, separated by comma
|
||||
- --domain / BIRDLG_DOMAIN: server name domain suffixes
|
||||
- --listen / BIRDLG_LISTEN: address bird-lg is listening on (default ":5000")
|
||||
- --proxy-port / BIRDLG_PROXY_PORT: port bird-lgproxy is running on (default 8000)
|
||||
- --whois / BIRDLG_WHOIS: whois server for queries (default "whois.verisign-grs.com")
|
||||
| Parameter | Environment Variable | Description |
|
||||
| --------- | -------------------- | ----------- |
|
||||
| --servers | BIRDLG_SERVERS | server name prefixes, separated by comma |
|
||||
| --domain | BIRDLG_DOMAIN | server name domain suffixes |
|
||||
| --listen | BIRDLG_LISTEN | address bird-lg is listening on (default ":5000") |
|
||||
| --proxy-port | BIRDLG_PROXY_PORT | port bird-lgproxy is running on (default 8000) |
|
||||
| --whois | BIRDLG_WHOIS | whois server for queries (default "whois.verisign-grs.com") |
|
||||
| --dns-interface | BIRDLG_DNS_INTERFACE | dns zone to query ASN information (default "asn.cymru.com") |
|
||||
| --title-brand | BIRDLG_TITLE_BRAND | prefix of page titles in browser tabs (default "Bird-lg Go") |
|
||||
| --navbar-brand | BIRDLG_NAVBAR_BRAND | brand to show in the navigation bar (default "Bird-lg Go") |
|
||||
|
||||
Example: the following command starts the frontend with 2 BIRD nodes, with domain name "gigsgigscloud.dn42.lantian.pub" and "hostdare.dn42.lantian.pub", and proxies are running on port 8000 on both nodes.
|
||||
|
||||
@@ -43,25 +88,24 @@ 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.
|
||||
|
||||
Features implemented:
|
||||
|
||||
- Sending queries to BIRD and BIRD6
|
||||
- If you are using BIRDv2, simply point both `--bird` and `--bird6` to the only socket file of BIRDv2
|
||||
- Sending queries to BIRD
|
||||
- Sending "restrict" command to BIRD to prevent unauthorized changes
|
||||
- Executing traceroute command on Linux, FreeBSD and OpenBSD
|
||||
- Source IP restriction
|
||||
|
||||
Usage: all configuration is done via commandline parameters or environment variables, no config file.
|
||||
|
||||
- --allowed / ALLOWED_IPS: IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "")
|
||||
- --bird / BIRD_SOCKET: socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl")
|
||||
- --bird6 / BIRD6_SOCKET: socket file for bird6, set either in parameter or environment variable BIRD6_SOCKET (default "/var/run/bird/bird6.ctl")
|
||||
- --listen / BIRDLG_LISTEN: listen address, set either in parameter or environment variable BIRDLG_LISTEN (default ":8000")
|
||||
| Parameter | Environment Variable | Description |
|
||||
| --------- | -------------------- | ----------- |
|
||||
| --allowed | ALLOWED_IPS | IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") |
|
||||
| --bird | BIRD_SOCKET | socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl") |
|
||||
| --listen | BIRDLG_LISTEN | listen address, set either in parameter or environment variable BIRDLG_LISTEN (default ":8000") |
|
||||
|
||||
Example: start proxy with default configuration, should work "out of the box" on Debian 9 with BIRDv1:
|
||||
|
||||
@@ -69,7 +113,7 @@ Example: start proxy with default configuration, should work "out of the box" on
|
||||
|
||||
Example: start proxy with custom bird socket location:
|
||||
|
||||
./proxy --bird /run/bird.ctl --bird6 /run/bird6.ctl
|
||||
./proxy --bird /run/bird.ctl
|
||||
|
||||
Example: the following docker-compose.yml entry does the same as above, but by starting a Docker container:
|
||||
|
||||
@@ -79,20 +123,66 @@ Example: the following docker-compose.yml entry does the same as above, but by s
|
||||
restart: always
|
||||
volumes:
|
||||
- "/run/bird.ctl:/var/run/bird/bird.ctl"
|
||||
- "/run/bird6.ctl:/var/run/bird/bird6.ctl"
|
||||
ports:
|
||||
- "192.168.0.1:8000:8000"
|
||||
|
||||
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
|
||||
|
||||
### Display names
|
||||
|
||||
The server parameter is composed of server name prefixes, separated by comma. It also supports an extended syntax: It allows to define display names for the user interface that are different from the actual server names.
|
||||
|
||||
For instance, the two servers from the basic example can be displayed as "Gigs" and "Hostdare" using the following syntax (as known from email addresses):
|
||||
|
||||
./frontend --servers="Gigs<gigsgigscloud>,Hostdare<hostdare>" --domain=dn42.lantian.pub
|
||||
|
||||
### IP addresses
|
||||
|
||||
You may also specify IP addresses as server names when no domain is specified. IPv6 link local addresses are supported, too.
|
||||
|
||||
For example:
|
||||
|
||||
./frontend --servers="Prod<prod.mydomain.local>,Test1<fd88:dead:beef::1>,Test2<fe80::c%wg0>" --domain=
|
||||
|
||||
These three servers are displayed as "Prod", "Test1" and "Test2" in the user interface.
|
||||
|
||||
### API
|
||||
|
||||
The frontend provides an API for running BIRD/traceroute/whois queries.
|
||||
|
||||
See [API docs](API.md) for detailed information.
|
||||
|
||||
### 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/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
|
||||
|
||||
|
@@ -1,13 +0,0 @@
|
||||
FROM amd64/debian:buster
|
||||
|
||||
LABEL Lan Tian "lantian@lantian.pub"
|
||||
ENV GOOS=linux GOARCH=amd64
|
||||
WORKDIR /root
|
||||
COPY . .
|
||||
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y golang git \
|
||||
&& cd /root && go get github.com/gorilla/handlers && go build -o /frontend \
|
||||
&& cd / && rm -rf /root/* \
|
||||
&& apt-get -qq purge -y golang git \
|
||||
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
|
||||
|
||||
ENTRYPOINT ["/frontend"]
|
@@ -1,13 +0,0 @@
|
||||
FROM multiarch/debian-debootstrap:armhf-buster
|
||||
|
||||
LABEL Lan Tian "lantian@lantian.pub"
|
||||
ENV GOOS=linux GOARCH=arm
|
||||
WORKDIR /root
|
||||
COPY . .
|
||||
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y golang git \
|
||||
&& cd /root && go get github.com/gorilla/handlers && go build -o /frontend \
|
||||
&& cd / && rm -rf /root/* \
|
||||
&& apt-get -qq purge -y golang git \
|
||||
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
|
||||
|
||||
ENTRYPOINT ["/frontend"]
|
@@ -1,13 +0,0 @@
|
||||
FROM multiarch/debian-debootstrap:arm64-buster
|
||||
|
||||
LABEL Lan Tian "lantian@lantian.pub"
|
||||
ENV GOOS=linux GOARCH=arm64
|
||||
WORKDIR /root
|
||||
COPY . .
|
||||
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y golang git \
|
||||
&& cd /root && go get github.com/gorilla/handlers && go build -o /frontend \
|
||||
&& cd / && rm -rf /root/* \
|
||||
&& apt-get -qq purge -y golang git \
|
||||
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
|
||||
|
||||
ENTRYPOINT ["/frontend"]
|
@@ -1,13 +0,0 @@
|
||||
FROM i386/debian:buster
|
||||
|
||||
LABEL Lan Tian "lantian@lantian.pub"
|
||||
ENV GOOS=linux GOARCH=386
|
||||
WORKDIR /root
|
||||
COPY . .
|
||||
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y golang git \
|
||||
&& cd /root && go get github.com/gorilla/handlers && go build -o /frontend \
|
||||
&& cd / && rm -rf /root/* \
|
||||
&& apt-get -qq purge -y golang git \
|
||||
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
|
||||
|
||||
ENTRYPOINT ["/frontend"]
|
5
frontend/Makefile
Normal file
5
frontend/Makefile
Normal file
@@ -0,0 +1,5 @@
|
||||
.PHONY: all
|
||||
all:
|
||||
go get -u github.com/kevinburke/go-bindata/...
|
||||
go generate
|
||||
go build -ldflags "-w -s" -o frontend
|
130
frontend/api.go
Normal file
130
frontend/api.go
Normal file
@@ -0,0 +1,130 @@
|
||||
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,
|
||||
"server_list": apiServerListHandler,
|
||||
}
|
||||
|
||||
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 apiServerListHandler(request apiRequest) apiResponse {
|
||||
var response apiResponse
|
||||
|
||||
for _, server := range setting.servers {
|
||||
response.Result = append(response.Result, apiGenericResultPair{
|
||||
Server: server,
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
bytes, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return
|
||||
}
|
||||
w.Write(bytes)
|
||||
}
|
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
@@ -24,7 +25,7 @@ func birdRouteToGraphviz(servers []string, responses []string, target string) st
|
||||
graph := make(map[string]string)
|
||||
// Helper to add an edge
|
||||
addEdge := func(src string, dest string, attr string) {
|
||||
key := "\"" + src + "\" -> \"" + dest + "\""
|
||||
key := "\"" + html.EscapeString(src) + "\" -> \"" + html.EscapeString(dest) + "\""
|
||||
_, present := graph[key]
|
||||
// Do not remove edge's attributes if it's already present
|
||||
if present && len(attr) == 0 {
|
||||
@@ -34,7 +35,7 @@ func birdRouteToGraphviz(servers []string, responses []string, target string) st
|
||||
}
|
||||
// Helper to set attribute for a point in graph
|
||||
addPoint := func(name string, attr string) {
|
||||
key := "\"" + name + "\""
|
||||
key := "\"" + html.EscapeString(name) + "\""
|
||||
_, present := graph[key]
|
||||
// Do not remove point's attributes if it's already present
|
||||
if present && len(attr) == 0 {
|
||||
|
74
frontend/bgpmap_test.go
Normal file
74
frontend/bgpmap_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetASNRepresentation(t *testing.T) {
|
||||
setting.dnsInterface = "asn.cymru.com"
|
||||
result := getASNRepresentation("6939")
|
||||
if !strings.Contains(result, "HURRICANE") {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetASNRepresentationFallback(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
result := getASNRepresentation("6939")
|
||||
if result != "AS6939" {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBirdRouteToGraphviz(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
|
||||
// Don't change formatting of the following strings!
|
||||
|
||||
fakeResult := `192.168.0.1/32 unicast [alpha 2021-01-14 from 192.168.0.2] * (100) [AS12345i]
|
||||
via 192.168.0.2 on eth0
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242422601
|
||||
BGP.next_hop: 172.18.0.2`
|
||||
|
||||
expectedResult := `digraph {
|
||||
"Nexthop:\n172.18.0.2" -> "AS4242422601" [color=red];
|
||||
"Nexthop:\n172.18.0.2" [shape=diamond];
|
||||
"AS4242422601" -> "Target: 192.168.0.1" [color=red];
|
||||
"Target: 192.168.0.1" [color=red,shape=diamond];
|
||||
"alpha" [color=blue,shape=box];
|
||||
"alpha" -> "Nexthop:\n172.18.0.2" [color=red];
|
||||
}`
|
||||
|
||||
result := birdRouteToGraphviz([]string{
|
||||
"alpha",
|
||||
}, []string{
|
||||
fakeResult,
|
||||
}, "192.168.0.1")
|
||||
|
||||
for _, line := range strings.Split(result, "\n") {
|
||||
if !strings.Contains(expectedResult, line) {
|
||||
t.Errorf("Unexpected line in result: %s", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBirdRouteToGraphvizXSS(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
|
||||
// Don't change formatting of the following strings!
|
||||
|
||||
fakeResult := `<script>alert("evil!")</script>`
|
||||
|
||||
result := birdRouteToGraphviz([]string{
|
||||
"alpha",
|
||||
}, []string{
|
||||
fakeResult,
|
||||
}, fakeResult)
|
||||
|
||||
if strings.Contains(result, "<script>") {
|
||||
t.Errorf("XSS injection succeeded: %s", result)
|
||||
}
|
||||
}
|
2
frontend/bindata/robots.txt
Normal file
2
frontend/bindata/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
16
frontend/bindata/templates/bgpmap.tpl
Normal file
16
frontend/bindata/templates/bgpmap.tpl
Normal file
@@ -0,0 +1,16 @@
|
||||
<h2>BGPmap: {{ html .Target }}</h2>
|
||||
<div id="bgpmap">
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
var viz = new Viz();
|
||||
viz.renderSVGElement(`{{ .Result }}`)
|
||||
.then(element => {
|
||||
document.getElementById("bgpmap").appendChild(element);
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById("bgpmap").innerHTML = "<pre>"+error+"</pre>"
|
||||
});
|
||||
</script>
|
2
frontend/bindata/templates/bird.tpl
Normal file
2
frontend/bindata/templates/bird.tpl
Normal file
@@ -0,0 +1,2 @@
|
||||
<h2>{{ html .ServerName }}: {{ html .Target }}</h2>
|
||||
{{ .Result }}
|
87
frontend/bindata/templates/page.tpl
Normal file
87
frontend/bindata/templates/page.tpl
Normal file
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="renderer" content="webkit">
|
||||
<title>{{ html .Title }}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="/">{{ .Brand }}</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
{{ $option := .URLOption }}
|
||||
{{ $server := .URLServer }}
|
||||
{{ $target := .URLCommand }}
|
||||
{{ if .IsWhois }}
|
||||
{{ $option = "summary" }}
|
||||
{{ $server = .AllServersURL }}
|
||||
{{ $target = "" }}
|
||||
{{ end }}
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}"
|
||||
href="/{{ $option }}/{{ .AllServersURL }}/{{ $target }}"> All Servers </a>
|
||||
</li>
|
||||
{{ range $k, $v := .ServersEscaped }}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
|
||||
href="/{{ $option }}/{{ $v }}/{{ $target }}">{{ html (index $.ServersDisplay $k) }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ if .IsWhois }}
|
||||
{{ $target = .WhoisTarget }}
|
||||
{{ end }}
|
||||
<form name="goto" class="form-inline" action="javascript:goto();">
|
||||
<div class="input-group">
|
||||
<select name="action" class="form-control">
|
||||
{{ range $k, $v := .Options }}
|
||||
<option value="{{ html $k }}"{{ if eq $k $.URLOption }} selected{{end}}>{{ html $v }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<input name="server" class="d-none" value="{{ html $server }}">
|
||||
<input name="target" class="form-control" placeholder="Target" aria-label="Target" value="{{ html $target }}">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-success" type="submit">»</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
function goto() {
|
||||
let action = $('[name="action"]').val();
|
||||
let server = $('[name="server"]').val();
|
||||
let target = $('[name="target"]').val();
|
||||
let url = "";
|
||||
|
||||
if (action == "whois") {
|
||||
url = "/" + action + "/" + target;
|
||||
} else if (action == "summary") {
|
||||
url = "/" + action + "/" + server + "/";
|
||||
} else {
|
||||
url = "/" + action + "/" + server + "/" + target;
|
||||
}
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
21
frontend/bindata/templates/summary.tpl
Normal file
21
frontend/bindata/templates/summary.tpl
Normal file
@@ -0,0 +1,21 @@
|
||||
{{ $ServerName := urlquery .ServerName }}
|
||||
|
||||
<table class="table table-striped table-bordered table-sm">
|
||||
<thead>
|
||||
{{ range .Header }}
|
||||
<th scope="col">{{ html . }}</th>
|
||||
{{ end }}
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Rows }}
|
||||
<tr class="table-{{ .MappedState }}">
|
||||
<td><a href="/detail/{{ $ServerName }}/{{ urlquery .Name }}">{{ html .Name }}</a></td>
|
||||
<td>{{ html .Proto }}</td>
|
||||
<td>{{ html .Table }}</td>
|
||||
<td>{{ html .State }}</td>
|
||||
<td>{{ html .Since }}</td>
|
||||
<td>{{ html .Info }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
2
frontend/bindata/templates/whois.tpl
Normal file
2
frontend/bindata/templates/whois.tpl
Normal file
@@ -0,0 +1,2 @@
|
||||
<h2>whois {{ html .Target }}</h2>
|
||||
{{ .Result }}
|
30
frontend/dn42_test.go
Normal file
30
frontend/dn42_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDN42WhoisFilter(t *testing.T) {
|
||||
input := "name: Testing\ndescr: Description"
|
||||
|
||||
result := dn42WhoisFilter(input)
|
||||
|
||||
expectedResult := `name: Testing
|
||||
|
||||
1 line(s) skipped.
|
||||
`
|
||||
|
||||
if result != expectedResult {
|
||||
t.Errorf("Output doesn't match expected: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDN42WhoisFilterUnneeded(t *testing.T) {
|
||||
input := "name: Testing\nwhatever: Description"
|
||||
|
||||
result := dn42WhoisFilter(input)
|
||||
|
||||
if result != input+"\n" {
|
||||
t.Errorf("Output doesn't match expected: %s", result)
|
||||
}
|
||||
}
|
9
frontend/go.mod
Normal file
9
frontend/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module github.com/xddxdd/bird-lg-go/frontend
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/kevinburke/go-bindata v3.22.0+incompatible // indirect
|
||||
)
|
8
frontend/go.sum
Normal file
8
frontend/go.sum
Normal file
@@ -0,0 +1,8 @@
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/kevinburke/go-bindata v3.22.0+incompatible h1:/JmqEhIWQ7GRScV0WjX/0tqBrC5D21ALg0H0U/KZ/ts=
|
||||
github.com/kevinburke/go-bindata v3.22.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM=
|
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type channelData struct {
|
||||
@@ -35,7 +36,15 @@ func batchRequest(servers []string, endpoint string, command string) []string {
|
||||
}(i)
|
||||
} else {
|
||||
// Compose URL and send the request
|
||||
url := "http://" + server + "." + setting.domain + ":" + strconv.Itoa(setting.proxyPort) + "/" + url.PathEscape(endpoint) + "?q=" + url.QueryEscape(command)
|
||||
hostname := server
|
||||
hostname = url.PathEscape(hostname)
|
||||
if strings.Contains(hostname, ":") {
|
||||
hostname = "[" + hostname + "]"
|
||||
}
|
||||
if setting.domain != "" {
|
||||
hostname += "." + setting.domain
|
||||
}
|
||||
url := "http://" + hostname + ":" + strconv.Itoa(setting.proxyPort) + "/" + url.PathEscape(endpoint) + "?q=" + url.QueryEscape(command)
|
||||
go func(url string, i int) {
|
||||
response, err := http.Get(url)
|
||||
if err != nil {
|
||||
|
@@ -7,8 +7,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// binary data
|
||||
//go:generate go-bindata -prefix bindata -o bindata.go bindata/...
|
||||
|
||||
type settingType struct {
|
||||
servers []string
|
||||
serversDisplay []string
|
||||
domain string
|
||||
proxyPort int
|
||||
whoisServer string
|
||||
@@ -77,12 +81,23 @@ func main() {
|
||||
|
||||
if *serversPtr == "" {
|
||||
panic("no server set")
|
||||
} else if *domainPtr == "" {
|
||||
panic("no base domain set")
|
||||
}
|
||||
|
||||
servers := strings.Split(*serversPtr, ",")
|
||||
serversDisplay := strings.Split(*serversPtr, ",")
|
||||
|
||||
// Split server names of the form "DisplayName<Hostname>"
|
||||
for i, server := range servers {
|
||||
pos := strings.Index(server, "<")
|
||||
if pos != -1 {
|
||||
serversDisplay[i] = server[0:pos]
|
||||
servers[i] = server[pos+1:len(server)-1]
|
||||
}
|
||||
}
|
||||
|
||||
setting = settingType{
|
||||
strings.Split(*serversPtr, ","),
|
||||
servers,
|
||||
serversDisplay,
|
||||
*domainPtr,
|
||||
*proxyPortPtr,
|
||||
*whoisPtr,
|
||||
@@ -93,5 +108,6 @@ func main() {
|
||||
*navBarBrandPtr,
|
||||
}
|
||||
|
||||
ImportTemplates()
|
||||
webServerStart()
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
79
frontend/render_test.go
Normal file
79
frontend/render_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func initSettings() {
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.serversDisplay = []string{"alpha"}
|
||||
setting.titleBrand = "Bird-lg Go"
|
||||
setting.navBarBrand = "Bird-lg Go"
|
||||
|
||||
ImportTemplates()
|
||||
}
|
||||
|
||||
func TestRenderPageTemplate(t *testing.T) {
|
||||
initSettings()
|
||||
|
||||
title := "Test Title"
|
||||
content := "Test Content"
|
||||
|
||||
r := httptest.NewRequest("GET", "/route/alpha/192.168.0.1/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
renderPageTemplate(w, r, title, content)
|
||||
|
||||
resultBytes, _ := ioutil.ReadAll(w.Result().Body)
|
||||
result := string(resultBytes)
|
||||
|
||||
if !strings.Contains(result, title) {
|
||||
t.Error("Title not found in output")
|
||||
}
|
||||
if !strings.Contains(result, content) {
|
||||
t.Error("Content not found in output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderPageTemplateXSS(t *testing.T) {
|
||||
initSettings()
|
||||
|
||||
evil := "<script>alert('evil');</script>"
|
||||
|
||||
r := httptest.NewRequest("GET", "/whois/"+url.PathEscape(evil), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// renderPageTemplate doesn't escape content, filter is done beforehand
|
||||
renderPageTemplate(w, r, evil, "Test Content")
|
||||
|
||||
resultBytes, _ := ioutil.ReadAll(w.Result().Body)
|
||||
result := string(resultBytes)
|
||||
|
||||
if strings.Contains(result, evil) {
|
||||
t.Errorf("XSS injection succeeded: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmartFormatterXSS(t *testing.T) {
|
||||
evil := "<script>alert('evil');</script>"
|
||||
result := smartFormatter(evil)
|
||||
|
||||
if strings.Contains(result, evil) {
|
||||
t.Errorf("XSS injection succeeded: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryTableXSS(t *testing.T) {
|
||||
evil := "<script>alert('evil');</script>"
|
||||
evilData := `Name Proto Table State Since Info
|
||||
` + evil + ` ` + evil + ` --- up 2021-01-04 17:21:44 ` + evil
|
||||
|
||||
result := summaryTable(evilData, evil)
|
||||
|
||||
if strings.Contains(result, evil) {
|
||||
t.Errorf("XSS injection succeeded: %s", result)
|
||||
}
|
||||
}
|
@@ -87,17 +87,13 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
||||
commandResult := ""
|
||||
|
||||
// - traceroute
|
||||
if telegramIsCommand(request.Message.Text, "trace") || telegramIsCommand(request.Message.Text, "trace4") {
|
||||
if telegramIsCommand(request.Message.Text, "trace") {
|
||||
commandResult = telegramBatchRequestFormat(servers, "traceroute", target, telegramDefaultPostProcess)
|
||||
} else if telegramIsCommand(request.Message.Text, "trace6") {
|
||||
commandResult = telegramBatchRequestFormat(servers, "traceroute6", target, telegramDefaultPostProcess)
|
||||
|
||||
} else if telegramIsCommand(request.Message.Text, "route") || telegramIsCommand(request.Message.Text, "route4") {
|
||||
} else if telegramIsCommand(request.Message.Text, "route") {
|
||||
commandResult = telegramBatchRequestFormat(servers, "bird", "show route for "+target+" primary", telegramDefaultPostProcess)
|
||||
} else if telegramIsCommand(request.Message.Text, "route6") {
|
||||
commandResult = telegramBatchRequestFormat(servers, "bird6", "show route for "+target+" primary", telegramDefaultPostProcess)
|
||||
|
||||
} else if telegramIsCommand(request.Message.Text, "path") || telegramIsCommand(request.Message.Text, "path4") {
|
||||
} else if telegramIsCommand(request.Message.Text, "path") {
|
||||
commandResult = telegramBatchRequestFormat(servers, "bird", "show route for "+target+" all primary", func(result string) string {
|
||||
for _, s := range strings.Split(result, "\n") {
|
||||
if strings.Contains(s, "BGP.as_path: ") {
|
||||
@@ -106,15 +102,6 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
return ""
|
||||
})
|
||||
} else if telegramIsCommand(request.Message.Text, "path6") {
|
||||
commandResult = telegramBatchRequestFormat(servers, "bird6", "show route for "+target+" all primary", func(result string) string {
|
||||
for _, s := range strings.Split(result, "\n") {
|
||||
if strings.Contains(s, "BGP.as_path: ") {
|
||||
return strings.TrimSpace(strings.Split(s, ":")[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
} else if telegramIsCommand(request.Message.Text, "whois") {
|
||||
if setting.netSpecificMode == "dn42" {
|
||||
@@ -137,9 +124,9 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
} else if telegramIsCommand(request.Message.Text, "help") {
|
||||
commandResult = `
|
||||
/[path|path6] <IP>
|
||||
/[route|route6] <IP>
|
||||
/[trace|trace6] <IP>
|
||||
/path <IP>
|
||||
/route <IP>
|
||||
/trace <IP>
|
||||
/whois <Target>
|
||||
`
|
||||
} else {
|
||||
@@ -151,6 +138,10 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
||||
commandResult = "empty result"
|
||||
}
|
||||
|
||||
if len(commandResult) > 4096 {
|
||||
commandResult = commandResult[0:4096]
|
||||
}
|
||||
|
||||
// Create a JSON response
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
response := &tgWebhookResponse{
|
||||
|
31
frontend/template.Dockerfile
Normal file
31
frontend/template.Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM golang:buster AS step_0
|
||||
|
||||
#if defined(ARCH_AMD64)
|
||||
ENV GOOS=linux GOARCH=amd64
|
||||
#elif defined(ARCH_I386)
|
||||
ENV GOOS=linux GOARCH=386
|
||||
#elif defined(ARCH_ARM32V7)
|
||||
ENV GOOS=linux GOARCH=arm
|
||||
#elif defined(ARCH_ARM64V8)
|
||||
ENV GOOS=linux GOARCH=arm64
|
||||
#elif defined(ARCH_PPC64LE)
|
||||
ENV GOOS=linux GOARCH=ppc64le
|
||||
#elif defined(ARCH_S390X)
|
||||
ENV GOOS=linux GOARCH=s390x
|
||||
#else
|
||||
#error "Architecture not set"
|
||||
#endif
|
||||
|
||||
ENV CGO_ENABLED=0 GO111MODULE=on
|
||||
WORKDIR /root
|
||||
COPY . .
|
||||
# go-bindata is run on the build host as part of the go generate step
|
||||
RUN GOARCH=amd64 go get -u github.com/kevinburke/go-bindata/...
|
||||
RUN go generate
|
||||
RUN go build -ldflags "-w -s" -o /frontend
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM scratch AS step_1
|
||||
COPY --from=step_0 /frontend /
|
||||
ENTRYPOINT ["/frontend"]
|
@@ -1,13 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type tmplArguments struct {
|
||||
// template argument structures
|
||||
|
||||
// page
|
||||
type TemplatePage struct {
|
||||
// Global options
|
||||
Options map[string]string
|
||||
Servers []string
|
||||
ServersEscaped []string
|
||||
ServersDisplay []string
|
||||
|
||||
// Parameters related to current request
|
||||
AllServersLinkActive bool
|
||||
@@ -17,7 +23,6 @@ type tmplArguments struct {
|
||||
IsWhois bool
|
||||
WhoisTarget string
|
||||
|
||||
URLProto string
|
||||
URLOption string
|
||||
URLServer string
|
||||
URLCommand string
|
||||
@@ -28,70 +33,89 @@ type tmplArguments struct {
|
||||
Content string
|
||||
}
|
||||
|
||||
var tmpl = template.Must(template.New("tmpl").Parse(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="renderer" content="webkit">
|
||||
<title>{{ .Title }}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
</head>
|
||||
<body>
|
||||
// summary
|
||||
type SummaryRowData struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="/">{{ .Brand }}</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
// utility functions to allow filtering of results in the template
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item"><a class="nav-link{{ if eq "ipv4" .URLProto }} active{{ end }}" href="/ipv4/{{ .URLOption }}/{{ .URLServer }}/{{ .URLCommand }}"> IPv4 </a></li>
|
||||
<li class="nav-item"><a class="nav-link{{ if eq "ipv6" .URLProto }} active{{ end }}" href="/ipv6/{{ .URLOption }}/{{ .URLServer }}/{{ .URLCommand }}"> IPv6 </a></li>
|
||||
<span class="navbar-text">|</span>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}" href="/{{ .URLProto }}/{{ .URLOption }}/{{ .AllServersURL }}/{{ .URLCommand }}"> All Servers </a>
|
||||
</li>
|
||||
{{ range $k, $v := .Servers }}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ if eq $.URLServer $v }} active{{ end }}" href="/{{ $.URLProto }}/{{ $.URLOption }}/{{ $v }}/{{ $.URLCommand }}">{{ $v }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ $option := .URLOption }}
|
||||
{{ $target := .URLCommand }}
|
||||
{{ if .IsWhois }}
|
||||
{{ $option = "whois" }}
|
||||
{{ $target = .WhoisTarget }}
|
||||
{{ end }}
|
||||
<form class="form-inline" action="/redir" method="GET">
|
||||
<div class="input-group">
|
||||
<select name="action" class="form-control">
|
||||
{{ range $k, $v := .Options }}
|
||||
<option value="{{ $k }}"{{ if eq $k $option }} selected{{end}}>{{ $v }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<input name="proto" class="d-none" value="{{ .URLProto }}">
|
||||
<input name="server" class="d-none" value="{{ .URLServer }}">
|
||||
<input name="target" class="form-control" placeholder="Target" aria-label="Target" value="{{ $target }}">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-success" type="submit">»</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
func (r SummaryRowData) NameHasPrefix(prefix string) bool {
|
||||
return strings.HasPrefix(r.Name, prefix)
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
func (r SummaryRowData) NameContains(prefix string) bool {
|
||||
return strings.Contains(r.Name, prefix)
|
||||
}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
type TemplateSummary struct {
|
||||
ServerName string
|
||||
Raw string
|
||||
Header []string
|
||||
Rows []SummaryRowData
|
||||
}
|
||||
|
||||
// whois
|
||||
type TemplateWhois struct {
|
||||
Target string
|
||||
Result string
|
||||
}
|
||||
|
||||
// bgpmap
|
||||
type TemplateBGPmap struct {
|
||||
Servers []string
|
||||
Target string
|
||||
Result string
|
||||
}
|
||||
|
||||
// bird
|
||||
type TemplateBird struct {
|
||||
ServerName string
|
||||
Target string
|
||||
Result string
|
||||
}
|
||||
|
||||
// global variable to hold the templates
|
||||
|
||||
var TemplateLibrary map[string]*template.Template
|
||||
|
||||
// list of required templates
|
||||
|
||||
var requiredTemplates = [...]string{
|
||||
"page",
|
||||
"summary",
|
||||
"whois",
|
||||
"bgpmap",
|
||||
"bird",
|
||||
}
|
||||
|
||||
// import templates from bindata
|
||||
|
||||
func ImportTemplates() {
|
||||
|
||||
// create a new (blank) initial template
|
||||
TemplateLibrary = make(map[string]*template.Template)
|
||||
|
||||
// for each template that is needed
|
||||
for _, tmpl := range requiredTemplates {
|
||||
|
||||
// extract the template definition from the bindata
|
||||
def := MustAssetString("templates/" + tmpl + ".tpl")
|
||||
|
||||
// and add it to the template library
|
||||
template, err := template.New(tmpl).Parse(def)
|
||||
if err != nil {
|
||||
panic("Unable to parse template (templates/" + tmpl + ": " + err.Error())
|
||||
}
|
||||
|
||||
// store in the library
|
||||
TemplateLibrary[tmpl] = template
|
||||
}
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user