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
|
.DS_Store
|
||||||
frontend/frontend
|
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.
|
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.
|
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.
|
Usage: all configuration is done via commandline parameters or environment variables, no config file.
|
||||||
|
|
||||||
- --servers / BIRDLG_SERVERS: server name prefixes, separated by comma
|
| Parameter | Environment Variable | Description |
|
||||||
- --domain / BIRDLG_DOMAIN: server name domain suffixes
|
| --------- | -------------------- | ----------- |
|
||||||
- --listen / BIRDLG_LISTEN: address bird-lg is listening on (default ":5000")
|
| --servers | BIRDLG_SERVERS | server name prefixes, separated by comma |
|
||||||
- --proxy-port / BIRDLG_PROXY_PORT: port bird-lgproxy is running on (default 8000)
|
| --domain | BIRDLG_DOMAIN | server name domain suffixes |
|
||||||
- --whois / BIRDLG_WHOIS: whois server for queries (default "whois.verisign-grs.com")
|
| --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.
|
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
|
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.
|
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:
|
Features implemented:
|
||||||
|
|
||||||
- Sending queries to BIRD and BIRD6
|
- Sending queries to BIRD
|
||||||
- If you are using BIRDv2, simply point both `--bird` and `--bird6` to the only socket file of BIRDv2
|
|
||||||
- Sending "restrict" command to BIRD to prevent unauthorized changes
|
- Sending "restrict" command to BIRD to prevent unauthorized changes
|
||||||
- Executing traceroute command on Linux, FreeBSD and OpenBSD
|
- Executing traceroute command on Linux, FreeBSD and OpenBSD
|
||||||
- Source IP restriction
|
- Source IP restriction
|
||||||
|
|
||||||
Usage: all configuration is done via commandline parameters or environment variables, no config file.
|
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 "")
|
| Parameter | Environment Variable | Description |
|
||||||
- --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")
|
| --allowed | ALLOWED_IPS | IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") |
|
||||||
- --listen / BIRDLG_LISTEN: listen address, set either in parameter or environment variable BIRDLG_LISTEN (default ":8000")
|
| --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:
|
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:
|
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:
|
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
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- "/run/bird.ctl:/var/run/bird/bird.ctl"
|
- "/run/bird.ctl:/var/run/bird/bird.ctl"
|
||||||
- "/run/bird6.ctl:/var/run/bird/bird6.ctl"
|
|
||||||
ports:
|
ports:
|
||||||
- "192.168.0.1:8000:8000"
|
- "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.
|
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)
|
- Mehdi Abaakouk for creating [the original bird-lg project](https://github.com/sileht/bird-lg)
|
||||||
- [Bootstrap](https://getbootstrap.com/) as web UI framework
|
- [Bootstrap](https://getbootstrap.com/) as web UI framework
|
||||||
|
|
||||||
License
|
## License
|
||||||
-------
|
|
||||||
|
|
||||||
GPL 3.0
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -24,7 +25,7 @@ func birdRouteToGraphviz(servers []string, responses []string, target string) st
|
|||||||
graph := make(map[string]string)
|
graph := make(map[string]string)
|
||||||
// Helper to add an edge
|
// Helper to add an edge
|
||||||
addEdge := func(src string, dest string, attr string) {
|
addEdge := func(src string, dest string, attr string) {
|
||||||
key := "\"" + src + "\" -> \"" + dest + "\""
|
key := "\"" + html.EscapeString(src) + "\" -> \"" + html.EscapeString(dest) + "\""
|
||||||
_, present := graph[key]
|
_, present := graph[key]
|
||||||
// Do not remove edge's attributes if it's already present
|
// Do not remove edge's attributes if it's already present
|
||||||
if present && len(attr) == 0 {
|
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
|
// Helper to set attribute for a point in graph
|
||||||
addPoint := func(name string, attr string) {
|
addPoint := func(name string, attr string) {
|
||||||
key := "\"" + name + "\""
|
key := "\"" + html.EscapeString(name) + "\""
|
||||||
_, present := graph[key]
|
_, present := graph[key]
|
||||||
// Do not remove point's attributes if it's already present
|
// Do not remove point's attributes if it's already present
|
||||||
if present && len(attr) == 0 {
|
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/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type channelData struct {
|
type channelData struct {
|
||||||
@@ -35,7 +36,15 @@ func batchRequest(servers []string, endpoint string, command string) []string {
|
|||||||
}(i)
|
}(i)
|
||||||
} else {
|
} else {
|
||||||
// Compose URL and send the request
|
// 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) {
|
go func(url string, i int) {
|
||||||
response, err := http.Get(url)
|
response, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// binary data
|
||||||
|
//go:generate go-bindata -prefix bindata -o bindata.go bindata/...
|
||||||
|
|
||||||
type settingType struct {
|
type settingType struct {
|
||||||
servers []string
|
servers []string
|
||||||
|
serversDisplay []string
|
||||||
domain string
|
domain string
|
||||||
proxyPort int
|
proxyPort int
|
||||||
whoisServer string
|
whoisServer string
|
||||||
@@ -77,12 +81,23 @@ func main() {
|
|||||||
|
|
||||||
if *serversPtr == "" {
|
if *serversPtr == "" {
|
||||||
panic("no server set")
|
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{
|
setting = settingType{
|
||||||
strings.Split(*serversPtr, ","),
|
servers,
|
||||||
|
serversDisplay,
|
||||||
*domainPtr,
|
*domainPtr,
|
||||||
*proxyPortPtr,
|
*proxyPortPtr,
|
||||||
*whoisPtr,
|
*whoisPtr,
|
||||||
@@ -93,5 +108,6 @@ func main() {
|
|||||||
*navBarBrandPtr,
|
*navBarBrandPtr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImportTemplates()
|
||||||
webServerStart()
|
webServerStart()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,89 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
func renderTemplate(w http.ResponseWriter, r *http.Request, title string, content string) {
|
// static options map
|
||||||
|
var optionsMap = map[string]string{
|
||||||
|
"summary": "show protocols",
|
||||||
|
"detail": "show protocols all",
|
||||||
|
"route": "show route for ...",
|
||||||
|
"route_all": "show route for ... all",
|
||||||
|
"route_bgpmap": "show route for ... (bgpmap)",
|
||||||
|
"route_where": "show route where net ~ [ ... ]",
|
||||||
|
"route_where_all": "show route where net ~ [ ... ] all",
|
||||||
|
"route_where_bgpmap": "show route where net ~ [ ... ] (bgpmap)",
|
||||||
|
"route_generic": "show route ...",
|
||||||
|
"generic": "show ...",
|
||||||
|
"whois": "whois ...",
|
||||||
|
"traceroute": "traceroute ...",
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre-compiled regexp and constant statemap for summary rendering
|
||||||
|
var splitSummaryLine = regexp.MustCompile(`(\w+)(\s+)(\w+)(\s+)([\w-]+)(\s+)(\w+)(\s+)([0-9\-\. :]+)(.*)`)
|
||||||
|
var summaryStateMap = map[string]string{
|
||||||
|
"up": "success",
|
||||||
|
"down": "secondary",
|
||||||
|
"start": "danger",
|
||||||
|
"passive": "info",
|
||||||
|
}
|
||||||
|
|
||||||
|
// render the page template
|
||||||
|
func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, content string) {
|
||||||
path := r.URL.Path[1:]
|
path := r.URL.Path[1:]
|
||||||
split := strings.SplitN(path, "/", 4)
|
split := strings.SplitN(path, "/", 3)
|
||||||
|
|
||||||
isWhois := strings.ToLower(split[0]) == "whois"
|
isWhois := strings.ToLower(split[0]) == "whois"
|
||||||
whoisTarget := strings.Join(split[1:], "/")
|
whoisTarget := strings.Join(split[1:], "/")
|
||||||
|
|
||||||
// Use a default URL if the request URL is too short
|
// Use a default URL if the request URL is too short
|
||||||
// The URL is for return to IPv4 summary page
|
// The URL is for return to summary page
|
||||||
if len(split) < 3 {
|
if len(split) < 2 {
|
||||||
path = "ipv4/summary/" + strings.Join(setting.servers, "+") + "/"
|
path = "summary/" + url.PathEscape(strings.Join(setting.servers, "+")) + "/"
|
||||||
} else if len(split) == 3 {
|
} else if len(split) == 2 {
|
||||||
path += "/"
|
path += "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
split = strings.SplitN(path, "/", 4)
|
split = strings.SplitN(path, "/", 3)
|
||||||
|
|
||||||
var args tmplArguments
|
serversEscaped := make([]string, len(setting.servers))
|
||||||
args.Options = map[string]string{
|
for i, v := range setting.servers {
|
||||||
"summary": "show protocols",
|
serversEscaped[i] = url.PathEscape(v)
|
||||||
"detail": "show protocols all",
|
|
||||||
"route": "show route for ...",
|
|
||||||
"route_all": "show route for ... all",
|
|
||||||
"route_bgpmap": "show route for ... (bgpmap)",
|
|
||||||
"route_where": "show route where net ~ [ ... ]",
|
|
||||||
"route_where_all": "show route where net ~ [ ... ] all",
|
|
||||||
"route_where_bgpmap": "show route where net ~ [ ... ] (bgpmap)",
|
|
||||||
"route_generic": "show route ...",
|
|
||||||
"generic": "show ...",
|
|
||||||
"whois": "whois ...",
|
|
||||||
"traceroute": "traceroute ...",
|
|
||||||
}
|
}
|
||||||
args.Servers = setting.servers
|
|
||||||
args.AllServersLinkActive = strings.ToLower(split[2]) == strings.ToLower(strings.Join(setting.servers, "+"))
|
|
||||||
args.AllServersURL = strings.Join(setting.servers, "+")
|
|
||||||
args.IsWhois = isWhois
|
|
||||||
args.WhoisTarget = whoisTarget
|
|
||||||
|
|
||||||
args.URLProto = strings.ToLower(split[0])
|
args := TemplatePage{
|
||||||
args.URLOption = strings.ToLower(split[1])
|
Options: optionsMap,
|
||||||
args.URLServer = strings.ToLower(split[2])
|
Servers: setting.servers,
|
||||||
args.URLCommand = split[3]
|
ServersEscaped: serversEscaped,
|
||||||
|
ServersDisplay: setting.serversDisplay,
|
||||||
|
AllServersLinkActive: strings.ToLower(split[1]) == strings.ToLower(strings.Join(setting.servers, "+")),
|
||||||
|
AllServersURL: url.PathEscape(strings.Join(setting.servers, "+")),
|
||||||
|
IsWhois: isWhois,
|
||||||
|
WhoisTarget: whoisTarget,
|
||||||
|
|
||||||
args.Title = setting.titleBrand + title
|
URLOption: strings.ToLower(split[0]),
|
||||||
args.Brand = setting.navBarBrand
|
URLServer: url.PathEscape(strings.ToLower(split[1])),
|
||||||
args.Content = content
|
URLCommand: split[2],
|
||||||
|
Title: setting.titleBrand + title,
|
||||||
|
Brand: setting.navBarBrand,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := TemplateLibrary["page"]
|
||||||
err := tmpl.Execute(w, args)
|
err := tmpl.Execute(w, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
fmt.Println("Error rendering page:", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the given text to http response, and add whois links for
|
// Write the given text to http response, and add whois links for
|
||||||
@@ -65,6 +91,7 @@ func renderTemplate(w http.ResponseWriter, r *http.Request, title string, conten
|
|||||||
func smartFormatter(s string) string {
|
func smartFormatter(s string) string {
|
||||||
var result string
|
var result string
|
||||||
result += "<pre>"
|
result += "<pre>"
|
||||||
|
s = template.HTMLEscapeString(s)
|
||||||
for _, line := range strings.Split(s, "\n") {
|
for _, line := range strings.Split(s, "\n") {
|
||||||
var lineFormatted string
|
var lineFormatted string
|
||||||
if strings.HasPrefix(strings.TrimSpace(line), "BGP.as_path:") || strings.HasPrefix(strings.TrimSpace(line), "Neighbor AS:") || strings.HasPrefix(strings.TrimSpace(line), "Local AS:") {
|
if strings.HasPrefix(strings.TrimSpace(line), "BGP.as_path:") || strings.HasPrefix(strings.TrimSpace(line), "Neighbor AS:") || strings.HasPrefix(strings.TrimSpace(line), "Local AS:") {
|
||||||
@@ -81,91 +108,91 @@ func smartFormatter(s string) string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
type summaryTableArguments struct {
|
// Parse bird show protocols result
|
||||||
Headers []string
|
func summaryParse(data string, serverName string) (TemplateSummary, error) {
|
||||||
Lines [][]string
|
args := TemplateSummary{
|
||||||
|
ServerName: serverName,
|
||||||
|
Raw: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(strings.TrimSpace(data), "\n")
|
||||||
|
if len(lines) <= 1 {
|
||||||
|
// Likely backend returned an error message
|
||||||
|
return args, errors.New(strings.TrimSpace(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the table header
|
||||||
|
for _, col := range strings.Split(lines[0], " ") {
|
||||||
|
colTrimmed := strings.TrimSpace(col)
|
||||||
|
if len(colTrimmed) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
args.Header = append(args.Header, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the remaining rows
|
||||||
|
rows := lines[1:]
|
||||||
|
sort.Strings(rows)
|
||||||
|
|
||||||
|
// parse each line
|
||||||
|
for _, line := range rows {
|
||||||
|
|
||||||
|
// Ignore empty lines
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a total of 6 columns from bird summary
|
||||||
|
lineSplitted := splitSummaryLine.FindStringSubmatch(line)
|
||||||
|
if lineSplitted == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var row SummaryRowData
|
||||||
|
|
||||||
|
if len(lineSplitted) >= 2 {
|
||||||
|
row.Name = strings.TrimSpace(lineSplitted[1])
|
||||||
|
}
|
||||||
|
if len(lineSplitted) >= 4 {
|
||||||
|
row.Proto = strings.TrimSpace(lineSplitted[3])
|
||||||
|
}
|
||||||
|
if len(lineSplitted) >= 6 {
|
||||||
|
row.Table = strings.TrimSpace(lineSplitted[5])
|
||||||
|
}
|
||||||
|
if len(lineSplitted) >= 8 {
|
||||||
|
row.State = strings.TrimSpace(lineSplitted[7])
|
||||||
|
row.MappedState = summaryStateMap[row.State]
|
||||||
|
}
|
||||||
|
if len(lineSplitted) >= 10 {
|
||||||
|
row.Since = strings.TrimSpace(lineSplitted[9])
|
||||||
|
}
|
||||||
|
if len(lineSplitted) >= 11 {
|
||||||
|
row.Info = strings.TrimSpace(lineSplitted[10])
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to the result
|
||||||
|
args.Rows = append(args.Rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output a table for the summary page
|
// Output a table for the summary page
|
||||||
func summaryTable(isIPv6 bool, data string, serverName string) string {
|
func summaryTable(data string, serverName string) string {
|
||||||
var result string
|
result, err := summaryParse(data, serverName)
|
||||||
|
|
||||||
// Sort the table, excluding title row
|
if err != nil {
|
||||||
stringsSplitted := strings.Split(strings.TrimSpace(data), "\n")
|
return "<pre>" + template.HTMLEscapeString(err.Error()) + "</pre>"
|
||||||
if len(stringsSplitted) <= 1 {
|
|
||||||
// Likely backend returned an error message
|
|
||||||
result = "<pre>" + strings.TrimSpace(data) + "</pre>"
|
|
||||||
} else {
|
|
||||||
// Draw the table head
|
|
||||||
result += `<table class="table table-striped table-bordered table-sm">`
|
|
||||||
result += `<thead>`
|
|
||||||
for _, col := range strings.Split(stringsSplitted[0], " ") {
|
|
||||||
colTrimmed := strings.TrimSpace(col)
|
|
||||||
if len(colTrimmed) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += `<th scope="col">` + colTrimmed + `</th>`
|
|
||||||
}
|
|
||||||
result += `</thead><tbody>`
|
|
||||||
|
|
||||||
stringsWithoutTitle := stringsSplitted[1:]
|
|
||||||
sort.Strings(stringsWithoutTitle)
|
|
||||||
|
|
||||||
for _, line := range stringsWithoutTitle {
|
|
||||||
// Ignore empty lines
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if len(line) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse a total of 6 columns from bird summary
|
|
||||||
lineSplitted := regexp.MustCompile(`(\w+)(\s+)(\w+)(\s+)([\w-]+)(\s+)(\w+)(\s+)([0-9\-\. :]+)(.*)`).FindStringSubmatch(line)
|
|
||||||
if lineSplitted == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var row [6]string
|
|
||||||
if len(lineSplitted) >= 2 {
|
|
||||||
row[0] = strings.TrimSpace(lineSplitted[1])
|
|
||||||
}
|
|
||||||
if len(lineSplitted) >= 4 {
|
|
||||||
row[1] = strings.TrimSpace(lineSplitted[3])
|
|
||||||
}
|
|
||||||
if len(lineSplitted) >= 6 {
|
|
||||||
row[2] = strings.TrimSpace(lineSplitted[5])
|
|
||||||
}
|
|
||||||
if len(lineSplitted) >= 8 {
|
|
||||||
row[3] = strings.TrimSpace(lineSplitted[7])
|
|
||||||
}
|
|
||||||
if len(lineSplitted) >= 10 {
|
|
||||||
row[4] = strings.TrimSpace(lineSplitted[9])
|
|
||||||
}
|
|
||||||
if len(lineSplitted) >= 11 {
|
|
||||||
row[5] = strings.TrimSpace(lineSplitted[10])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the row in red if the link isn't up
|
|
||||||
result += `<tr class="` + (map[string]string{
|
|
||||||
"up": "table-success",
|
|
||||||
"down": "table-secondary",
|
|
||||||
"start": "table-danger",
|
|
||||||
"passive": "table-info",
|
|
||||||
})[row[3]] + `">`
|
|
||||||
// Add link to detail for first column
|
|
||||||
if isIPv6 {
|
|
||||||
result += `<td><a href="/ipv6/detail/` + serverName + `/` + row[0] + `">` + row[0] + `</a></td>`
|
|
||||||
} else {
|
|
||||||
result += `<td><a href="/ipv4/detail/` + serverName + `/` + row[0] + `">` + row[0] + `</a></td>`
|
|
||||||
}
|
|
||||||
// Draw the other cells
|
|
||||||
for i := 1; i < 6; i++ {
|
|
||||||
result += "<td>" + row[i] + "</td>"
|
|
||||||
}
|
|
||||||
result += "</tr>"
|
|
||||||
}
|
|
||||||
result += "</tbody></table>"
|
|
||||||
result += "<!--" + data + "-->"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
// render the summary template
|
||||||
|
tmpl := TemplateLibrary["summary"]
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err = tmpl.Execute(&buffer, result)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error rendering summary:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|||||||
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 := ""
|
commandResult := ""
|
||||||
|
|
||||||
// - traceroute
|
// - traceroute
|
||||||
if telegramIsCommand(request.Message.Text, "trace") || telegramIsCommand(request.Message.Text, "trace4") {
|
if telegramIsCommand(request.Message.Text, "trace") {
|
||||||
commandResult = telegramBatchRequestFormat(servers, "traceroute", target, telegramDefaultPostProcess)
|
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)
|
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 {
|
commandResult = telegramBatchRequestFormat(servers, "bird", "show route for "+target+" all primary", func(result string) string {
|
||||||
for _, s := range strings.Split(result, "\n") {
|
for _, s := range strings.Split(result, "\n") {
|
||||||
if strings.Contains(s, "BGP.as_path: ") {
|
if strings.Contains(s, "BGP.as_path: ") {
|
||||||
@@ -106,15 +102,6 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
return ""
|
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") {
|
} else if telegramIsCommand(request.Message.Text, "whois") {
|
||||||
if setting.netSpecificMode == "dn42" {
|
if setting.netSpecificMode == "dn42" {
|
||||||
@@ -137,9 +124,9 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
} else if telegramIsCommand(request.Message.Text, "help") {
|
} else if telegramIsCommand(request.Message.Text, "help") {
|
||||||
commandResult = `
|
commandResult = `
|
||||||
/[path|path6] <IP>
|
/path <IP>
|
||||||
/[route|route6] <IP>
|
/route <IP>
|
||||||
/[trace|trace6] <IP>
|
/trace <IP>
|
||||||
/whois <Target>
|
/whois <Target>
|
||||||
`
|
`
|
||||||
} else {
|
} else {
|
||||||
@@ -151,6 +138,10 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
|||||||
commandResult = "empty result"
|
commandResult = "empty result"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(commandResult) > 4096 {
|
||||||
|
commandResult = commandResult[0:4096]
|
||||||
|
}
|
||||||
|
|
||||||
// Create a JSON response
|
// Create a JSON response
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
response := &tgWebhookResponse{
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tmplArguments struct {
|
// template argument structures
|
||||||
|
|
||||||
|
// page
|
||||||
|
type TemplatePage struct {
|
||||||
// Global options
|
// Global options
|
||||||
Options map[string]string
|
Options map[string]string
|
||||||
Servers []string
|
Servers []string
|
||||||
|
ServersEscaped []string
|
||||||
|
ServersDisplay []string
|
||||||
|
|
||||||
// Parameters related to current request
|
// Parameters related to current request
|
||||||
AllServersLinkActive bool
|
AllServersLinkActive bool
|
||||||
@@ -17,7 +23,6 @@ type tmplArguments struct {
|
|||||||
IsWhois bool
|
IsWhois bool
|
||||||
WhoisTarget string
|
WhoisTarget string
|
||||||
|
|
||||||
URLProto string
|
|
||||||
URLOption string
|
URLOption string
|
||||||
URLServer string
|
URLServer string
|
||||||
URLCommand string
|
URLCommand string
|
||||||
@@ -28,70 +33,89 @@ type tmplArguments struct {
|
|||||||
Content string
|
Content string
|
||||||
}
|
}
|
||||||
|
|
||||||
var tmpl = template.Must(template.New("tmpl").Parse(`
|
// summary
|
||||||
<!DOCTYPE html>
|
type SummaryRowData struct {
|
||||||
<html lang="en-US">
|
Name string `json:"name"`
|
||||||
<head>
|
Proto string `json:"proto"`
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
Table string `json:"table"`
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
State string `json:"state"`
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
MappedState string `json:"-"`
|
||||||
<meta name="renderer" content="webkit">
|
Since string `json:"since"`
|
||||||
<title>{{ .Title }}</title>
|
Info string `json:"info"`
|
||||||
<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">
|
// utility functions to allow filtering of results in the template
|
||||||
<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">
|
func (r SummaryRowData) NameHasPrefix(prefix string) bool {
|
||||||
<ul class="navbar-nav mr-auto">
|
return strings.HasPrefix(r.Name, prefix)
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="container">
|
func (r SummaryRowData) NameContains(prefix string) bool {
|
||||||
{{ .Content }}
|
return strings.Contains(r.Name, prefix)
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
type TemplateSummary struct {
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
|
ServerName string
|
||||||
</body>
|
Raw string
|
||||||
</html>
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,47 +1,77 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func webHandlerWhois(w http.ResponseWriter, r *http.Request) {
|
var primitiveMap = map[string]string{
|
||||||
var target string = r.URL.Path[len("/whois/"):]
|
"summary": "show protocols",
|
||||||
|
"detail": "show protocols all %s",
|
||||||
|
"route": "show route for %s",
|
||||||
|
"route_all": "show route for %s all",
|
||||||
|
"route_where": "show route where net ~ [ %s ]",
|
||||||
|
"route_where_all": "show route where net ~ [ %s ] all",
|
||||||
|
"route_generic": "show route %s",
|
||||||
|
"generic": "show %s",
|
||||||
|
"traceroute": "%s",
|
||||||
|
}
|
||||||
|
|
||||||
renderTemplate(
|
// serve up a generic error
|
||||||
|
func serverError(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("500 Internal Server Error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHOIS pages
|
||||||
|
func webHandlerWhois(w http.ResponseWriter, r *http.Request) {
|
||||||
|
target, err := url.PathUnescape(r.URL.Path[len("/whois/"):])
|
||||||
|
if err != nil {
|
||||||
|
serverError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// render the whois template
|
||||||
|
args := TemplateWhois{
|
||||||
|
Target: target,
|
||||||
|
Result: smartFormatter(whois(target)),
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := TemplateLibrary["whois"]
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err = tmpl.Execute(&buffer, args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error rendering whois template:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPageTemplate(
|
||||||
w, r,
|
w, r,
|
||||||
" - whois "+html.EscapeString(target),
|
" - whois "+html.EscapeString(target),
|
||||||
"<h2>whois "+html.EscapeString(target)+"</h2>"+smartFormatter(whois(target)),
|
buffer.String(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serve up results from bird
|
||||||
func webBackendCommunicator(endpoint string, command string) func(w http.ResponseWriter, r *http.Request) {
|
func webBackendCommunicator(endpoint string, command string) func(w http.ResponseWriter, r *http.Request) {
|
||||||
backendCommandPrimitive, commandPresent := (map[string]string{
|
|
||||||
"summary": "show protocols",
|
|
||||||
"detail": "show protocols all %s",
|
|
||||||
"route": "show route for %s",
|
|
||||||
"route_all": "show route for %s all",
|
|
||||||
"route_where": "show route where net ~ [ %s ]",
|
|
||||||
"route_where_all": "show route where net ~ [ %s ] all",
|
|
||||||
"route_generic": "show route %s",
|
|
||||||
"generic": "show %s",
|
|
||||||
"traceroute": "%s",
|
|
||||||
})[command]
|
|
||||||
|
|
||||||
|
backendCommandPrimitive, commandPresent := primitiveMap[command]
|
||||||
if !commandPresent {
|
if !commandPresent {
|
||||||
panic("invalid command: " + command)
|
panic("invalid command: " + command)
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
split := strings.SplitN(r.URL.Path[1:], "/", 4)
|
split := strings.SplitN(r.URL.Path[1:], "/", 3)
|
||||||
var urlCommands string
|
var urlCommands string
|
||||||
if len(split) >= 4 {
|
if len(split) >= 3 {
|
||||||
urlCommands = split[3]
|
urlCommands = split[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
var backendCommand string
|
var backendCommand string
|
||||||
@@ -52,27 +82,53 @@ func webBackendCommunicator(endpoint string, command string) func(w http.Respons
|
|||||||
}
|
}
|
||||||
backendCommand = strings.TrimSpace(backendCommand)
|
backendCommand = strings.TrimSpace(backendCommand)
|
||||||
|
|
||||||
var servers []string = strings.Split(split[2], "+")
|
servers := strings.Split(split[1], "+")
|
||||||
|
|
||||||
var responses []string = batchRequest(servers, endpoint, backendCommand)
|
var responses []string = batchRequest(servers, endpoint, backendCommand)
|
||||||
var result string
|
var content string
|
||||||
for i, response := range responses {
|
for i, response := range responses {
|
||||||
result += "<h2>" + html.EscapeString(servers[i]) + ": " + html.EscapeString(backendCommand) + "</h2>"
|
|
||||||
if (endpoint == "bird" || endpoint == "bird6") && backendCommand == "show protocols" && len(response) > 4 && strings.ToLower(response[0:4]) == "name" {
|
var result string
|
||||||
var isIPv6 bool = endpoint[len(endpoint)-1] == '6'
|
if (endpoint == "bird") && backendCommand == "show protocols" && len(response) > 4 && strings.ToLower(response[0:4]) == "name" {
|
||||||
result += summaryTable(isIPv6, response, servers[i])
|
result = summaryTable(response, servers[i])
|
||||||
} else {
|
} else {
|
||||||
result += smartFormatter(response)
|
result = smartFormatter(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverDisplay := servers[i]
|
||||||
|
for k, v := range setting.servers {
|
||||||
|
if servers[i] == v {
|
||||||
|
serverDisplay = setting.serversDisplay[k]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// render the bird result template
|
||||||
|
args := TemplateBird{
|
||||||
|
ServerName: serverDisplay,
|
||||||
|
Target: backendCommand,
|
||||||
|
Result: result,
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := TemplateLibrary["bird"]
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err := tmpl.Execute(&buffer, args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error rendering bird template:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
content += buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTemplate(
|
renderPageTemplate(
|
||||||
w, r,
|
w, r,
|
||||||
" - "+html.EscapeString(endpoint+" "+backendCommand),
|
" - "+endpoint+" "+backendCommand,
|
||||||
result,
|
content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bgpmap result
|
||||||
func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWriter, r *http.Request) {
|
func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWriter, r *http.Request) {
|
||||||
backendCommandPrimitive, commandPresent := (map[string]string{
|
backendCommandPrimitive, commandPresent := (map[string]string{
|
||||||
"route_bgpmap": "show route for %s all",
|
"route_bgpmap": "show route for %s all",
|
||||||
@@ -85,7 +141,7 @@ func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWrite
|
|||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
split := strings.Split(r.URL.Path[1:], "/")
|
split := strings.Split(r.URL.Path[1:], "/")
|
||||||
urlCommands := strings.Join(split[3:], "/")
|
urlCommands := strings.Join(split[2:], "/")
|
||||||
|
|
||||||
var backendCommand string
|
var backendCommand string
|
||||||
if strings.Contains(backendCommandPrimitive, "%") {
|
if strings.Contains(backendCommandPrimitive, "%") {
|
||||||
@@ -94,68 +150,67 @@ func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWrite
|
|||||||
backendCommand = backendCommandPrimitive
|
backendCommand = backendCommandPrimitive
|
||||||
}
|
}
|
||||||
|
|
||||||
var servers []string = strings.Split(split[2], "+")
|
var servers []string = strings.Split(split[1], "+")
|
||||||
var responses []string = batchRequest(servers, endpoint, backendCommand)
|
var responses []string = batchRequest(servers, endpoint, backendCommand)
|
||||||
renderTemplate(
|
|
||||||
|
// render the bgpmap result template
|
||||||
|
args := TemplateBGPmap{
|
||||||
|
Servers: servers,
|
||||||
|
Target: backendCommand,
|
||||||
|
Result: birdRouteToGraphviz(servers, responses, urlCommands),
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := TemplateLibrary["bgpmap"]
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err := tmpl.Execute(&buffer, args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error rendering bgpmap template:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPageTemplate(
|
||||||
w, r,
|
w, r,
|
||||||
" - "+html.EscapeString(endpoint+" "+backendCommand),
|
" - "+html.EscapeString(endpoint+" "+backendCommand),
|
||||||
`
|
buffer.String(),
|
||||||
<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(`+"`"+birdRouteToGraphviz(servers, responses, urlCommands)+"`"+`)
|
|
||||||
.then(element => {
|
|
||||||
document.body.appendChild(element);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
document.body.innerHTML = "<pre>"+error+"</pre>"
|
|
||||||
});
|
|
||||||
</script>`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func webHandlerNavbarFormRedirect(w http.ResponseWriter, r *http.Request) {
|
// set up routing paths and start webserver
|
||||||
query := r.URL.Query()
|
|
||||||
if query.Get("action") == "whois" {
|
|
||||||
http.Redirect(w, r, "/"+query.Get("action")+"/"+query.Get("target"), 302)
|
|
||||||
} else if query.Get("action") == "summary" {
|
|
||||||
http.Redirect(w, r, "/"+query.Get("proto")+"/"+query.Get("action")+"/"+query.Get("server"), 302)
|
|
||||||
} else {
|
|
||||||
http.Redirect(w, r, "/"+query.Get("proto")+"/"+query.Get("action")+"/"+query.Get("server")+"/"+query.Get("target"), 302)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func webServerStart() {
|
func webServerStart() {
|
||||||
// Start HTTP server
|
|
||||||
|
// redirect main page to all server summary
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/ipv4/summary/"+strings.Join(setting.servers, "+"), 302)
|
http.Redirect(w, r, "/summary/"+url.PathEscape(strings.Join(setting.servers, "+")), 302)
|
||||||
})
|
})
|
||||||
http.HandleFunc("/ipv4/summary/", webBackendCommunicator("bird", "summary"))
|
|
||||||
http.HandleFunc("/ipv6/summary/", webBackendCommunicator("bird6", "summary"))
|
// serve static pages using the AssetFS and bindata
|
||||||
http.HandleFunc("/ipv4/detail/", webBackendCommunicator("bird", "detail"))
|
fs := http.FileServer(&assetfs.AssetFS{
|
||||||
http.HandleFunc("/ipv6/detail/", webBackendCommunicator("bird6", "detail"))
|
Asset: Asset,
|
||||||
http.HandleFunc("/ipv4/route/", webBackendCommunicator("bird", "route"))
|
AssetDir: AssetDir,
|
||||||
http.HandleFunc("/ipv6/route/", webBackendCommunicator("bird6", "route"))
|
AssetInfo: AssetInfo,
|
||||||
http.HandleFunc("/ipv4/route_all/", webBackendCommunicator("bird", "route_all"))
|
Prefix: "",
|
||||||
http.HandleFunc("/ipv6/route_all/", webBackendCommunicator("bird6", "route_all"))
|
})
|
||||||
http.HandleFunc("/ipv4/route_bgpmap/", webHandlerBGPMap("bird", "route_bgpmap"))
|
|
||||||
http.HandleFunc("/ipv6/route_bgpmap/", webHandlerBGPMap("bird6", "route_bgpmap"))
|
http.Handle("/static/", fs)
|
||||||
http.HandleFunc("/ipv4/route_where/", webBackendCommunicator("bird", "route_where"))
|
http.Handle("/robots.txt", fs)
|
||||||
http.HandleFunc("/ipv6/route_where/", webBackendCommunicator("bird6", "route_where"))
|
http.Handle("/favicon.ico", fs)
|
||||||
http.HandleFunc("/ipv4/route_where_all/", webBackendCommunicator("bird", "route_where_all"))
|
|
||||||
http.HandleFunc("/ipv6/route_where_all/", webBackendCommunicator("bird6", "route_where_all"))
|
// backend routes
|
||||||
http.HandleFunc("/ipv4/route_where_bgpmap/", webHandlerBGPMap("bird", "route_where_bgpmap"))
|
http.HandleFunc("/summary/", webBackendCommunicator("bird", "summary"))
|
||||||
http.HandleFunc("/ipv6/route_where_bgpmap/", webHandlerBGPMap("bird6", "route_where_bgpmap"))
|
http.HandleFunc("/detail/", webBackendCommunicator("bird", "detail"))
|
||||||
http.HandleFunc("/ipv4/route_generic/", webBackendCommunicator("bird", "route_generic"))
|
http.HandleFunc("/route/", webBackendCommunicator("bird", "route"))
|
||||||
http.HandleFunc("/ipv6/route_generic/", webBackendCommunicator("bird6", "route_generic"))
|
http.HandleFunc("/route_all/", webBackendCommunicator("bird", "route_all"))
|
||||||
http.HandleFunc("/ipv4/generic/", webBackendCommunicator("bird", "generic"))
|
http.HandleFunc("/route_bgpmap/", webHandlerBGPMap("bird", "route_bgpmap"))
|
||||||
http.HandleFunc("/ipv6/generic/", webBackendCommunicator("bird6", "generic"))
|
http.HandleFunc("/route_where/", webBackendCommunicator("bird", "route_where"))
|
||||||
http.HandleFunc("/ipv4/traceroute/", webBackendCommunicator("traceroute", "traceroute"))
|
http.HandleFunc("/route_where_all/", webBackendCommunicator("bird", "route_where_all"))
|
||||||
http.HandleFunc("/ipv6/traceroute/", webBackendCommunicator("traceroute6", "traceroute"))
|
http.HandleFunc("/route_where_bgpmap/", webHandlerBGPMap("bird", "route_where_bgpmap"))
|
||||||
|
http.HandleFunc("/route_generic/", webBackendCommunicator("bird", "route_generic"))
|
||||||
|
http.HandleFunc("/generic/", webBackendCommunicator("bird", "generic"))
|
||||||
|
http.HandleFunc("/traceroute/", webBackendCommunicator("traceroute", "traceroute"))
|
||||||
http.HandleFunc("/whois/", webHandlerWhois)
|
http.HandleFunc("/whois/", webHandlerWhois)
|
||||||
http.HandleFunc("/redir", webHandlerNavbarFormRedirect)
|
http.HandleFunc("/api/", apiHandler)
|
||||||
http.HandleFunc("/telegram/", webHandlerTelegramBot)
|
http.HandleFunc("/telegram/", webHandlerTelegramBot)
|
||||||
|
|
||||||
|
// Start HTTP server
|
||||||
http.ListenAndServe(setting.listen, handlers.LoggingHandler(os.Stdout, http.DefaultServeMux))
|
http.ListenAndServe(setting.listen, handlers.LoggingHandler(os.Stdout, http.DefaultServeMux))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +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 traceroute locales busybox \
|
|
||||||
&& cd /root && go get github.com/gorilla/handlers && go build -o /proxy \
|
|
||||||
&& cd / && rm -rf /root/* \
|
|
||||||
&& printf "en_US.UTF-8 UTF-8\nzh_CN.UTF-8 UTF-8\n" > /etc/locale.gen \
|
|
||||||
&& locale-gen \
|
|
||||||
&& apt-get -qq purge -y golang git \
|
|
||||||
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
|
|
||||||
|
|
||||||
ENTRYPOINT ["/proxy"]
|
|
||||||
@@ -1,15 +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 traceroute locales busybox \
|
|
||||||
&& cd /root && go get github.com/gorilla/handlers && go build -o /proxy \
|
|
||||||
&& cd / && rm -rf /root/* \
|
|
||||||
&& printf "en_US.UTF-8 UTF-8\nzh_CN.UTF-8 UTF-8\n" > /etc/locale.gen \
|
|
||||||
&& locale-gen \
|
|
||||||
&& apt-get -qq purge -y golang git \
|
|
||||||
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
|
|
||||||
|
|
||||||
ENTRYPOINT ["/proxy"]
|
|
||||||
@@ -1,15 +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 traceroute locales busybox \
|
|
||||||
&& cd /root && go get github.com/gorilla/handlers && go build -o /proxy \
|
|
||||||
&& cd / && rm -rf /root/* \
|
|
||||||
&& printf "en_US.UTF-8 UTF-8\nzh_CN.UTF-8 UTF-8\n" > /etc/locale.gen \
|
|
||||||
&& locale-gen \
|
|
||||||
&& apt-get -qq purge -y golang git \
|
|
||||||
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
|
|
||||||
|
|
||||||
ENTRYPOINT ["/proxy"]
|
|
||||||
@@ -1,15 +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 traceroute locales busybox \
|
|
||||||
&& cd /root && go get github.com/gorilla/handlers && go build -o /proxy \
|
|
||||||
&& cd / && rm -rf /root/* \
|
|
||||||
&& printf "en_US.UTF-8 UTF-8\nzh_CN.UTF-8 UTF-8\n" > /etc/locale.gen \
|
|
||||||
&& locale-gen \
|
|
||||||
&& apt-get -qq purge -y golang git \
|
|
||||||
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
|
|
||||||
|
|
||||||
ENTRYPOINT ["/proxy"]
|
|
||||||
3
proxy/Makefile
Normal file
3
proxy/Makefile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.PHONY: all
|
||||||
|
all: $(shell find . -name \*.go -type f)
|
||||||
|
go build -ldflags "-w -s" -o proxy
|
||||||
@@ -30,34 +30,16 @@ func birdReadln(bird io.Reader, w io.Writer) bool {
|
|||||||
// print(string(c[:]))
|
// print(string(c[:]))
|
||||||
|
|
||||||
// Remove preceding status number, different situations
|
// Remove preceding status number, different situations
|
||||||
if pos < 4 {
|
if pos > 4 && isNumeric(c[0]) && isNumeric(c[1]) && isNumeric(c[2]) && isNumeric(c[3]) {
|
||||||
// Line is too short to have a status number
|
|
||||||
if w != nil {
|
|
||||||
pos = 0
|
|
||||||
for c[pos] == byte(' ') {
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
w.Write(c[pos:])
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} else if isNumeric(c[0]) && isNumeric(c[1]) && isNumeric(c[2]) && isNumeric(c[3]) {
|
|
||||||
// There is a status number at beginning, remove first 5 bytes
|
// There is a status number at beginning, remove first 5 bytes
|
||||||
if w != nil && pos > 6 {
|
if w != nil && pos > 6 {
|
||||||
pos = 5
|
pos = 5
|
||||||
for c[pos] == byte(' ') {
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
w.Write(c[pos:])
|
w.Write(c[pos:])
|
||||||
}
|
}
|
||||||
return c[0] != byte('0') && c[0] != byte('8') && c[0] != byte('9')
|
return c[0] != byte('0') && c[0] != byte('8') && c[0] != byte('9')
|
||||||
} else {
|
} else {
|
||||||
// There is no status number, only remove preceding spaces
|
|
||||||
if w != nil {
|
if w != nil {
|
||||||
pos = 0
|
w.Write(c[1:])
|
||||||
for c[pos] == byte(' ') {
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
w.Write(c[pos:])
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -77,7 +59,9 @@ func birdHandler(httpW http.ResponseWriter, httpR *http.Request) {
|
|||||||
// Initialize BIRDv4 socket
|
// Initialize BIRDv4 socket
|
||||||
bird, err := net.Dial("unix", setting.birdSocket)
|
bird, err := net.Dial("unix", setting.birdSocket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
httpW.WriteHeader(http.StatusInternalServerError)
|
||||||
|
httpW.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
defer bird.Close()
|
defer bird.Close()
|
||||||
|
|
||||||
@@ -89,25 +73,3 @@ func birdHandler(httpW http.ResponseWriter, httpR *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles BIRDv6 queries
|
|
||||||
func bird6Handler(httpW http.ResponseWriter, httpR *http.Request) {
|
|
||||||
query := string(httpR.URL.Query().Get("q"))
|
|
||||||
if query == "" {
|
|
||||||
invalidHandler(httpW, httpR)
|
|
||||||
} else {
|
|
||||||
// Initialize BIRDv6 socket
|
|
||||||
bird6, err := net.Dial("unix", setting.bird6Socket)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer bird6.Close()
|
|
||||||
|
|
||||||
birdReadln(bird6, nil)
|
|
||||||
birdWriteln(bird6, "restrict")
|
|
||||||
birdReadln(bird6, nil)
|
|
||||||
birdWriteln(bird6, query)
|
|
||||||
for birdReadln(bird6, httpW) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
8
proxy/go.mod
Normal file
8
proxy/go.mod
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module github.com/xddxdd/bird-lg-go/proxy
|
||||||
|
|
||||||
|
go 1.15
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
|
github.com/gorilla/handlers v1.5.1
|
||||||
|
)
|
||||||
6
proxy/go.sum
Normal file
6
proxy/go.sum
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||||
|
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
|
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||||
|
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||||
@@ -50,10 +50,9 @@ func accessHandler(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type settingType struct {
|
type settingType struct {
|
||||||
birdSocket string
|
birdSocket string
|
||||||
bird6Socket string
|
listen string
|
||||||
listen string
|
allowedIPs []string
|
||||||
allowedIPs []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var setting settingType
|
var setting settingType
|
||||||
@@ -63,7 +62,6 @@ func main() {
|
|||||||
// Prepare default socket paths, use environment variable if possible
|
// Prepare default socket paths, use environment variable if possible
|
||||||
var settingDefault = settingType{
|
var settingDefault = settingType{
|
||||||
"/var/run/bird/bird.ctl",
|
"/var/run/bird/bird.ctl",
|
||||||
"/var/run/bird/bird6.ctl",
|
|
||||||
":8000",
|
":8000",
|
||||||
[]string{""},
|
[]string{""},
|
||||||
}
|
}
|
||||||
@@ -71,9 +69,6 @@ func main() {
|
|||||||
if birdSocketEnv := os.Getenv("BIRD_SOCKET"); birdSocketEnv != "" {
|
if birdSocketEnv := os.Getenv("BIRD_SOCKET"); birdSocketEnv != "" {
|
||||||
settingDefault.birdSocket = birdSocketEnv
|
settingDefault.birdSocket = birdSocketEnv
|
||||||
}
|
}
|
||||||
if bird6SocketEnv := os.Getenv("BIRD6_SOCKET"); bird6SocketEnv != "" {
|
|
||||||
settingDefault.bird6Socket = bird6SocketEnv
|
|
||||||
}
|
|
||||||
if listenEnv := os.Getenv("BIRDLG_LISTEN"); listenEnv != "" {
|
if listenEnv := os.Getenv("BIRDLG_LISTEN"); listenEnv != "" {
|
||||||
settingDefault.listen = listenEnv
|
settingDefault.listen = listenEnv
|
||||||
}
|
}
|
||||||
@@ -83,21 +78,19 @@ func main() {
|
|||||||
|
|
||||||
// Allow parameters to override environment variables
|
// Allow parameters to override environment variables
|
||||||
birdParam := flag.String("bird", settingDefault.birdSocket, "socket file for bird, set either in parameter or environment variable BIRD_SOCKET")
|
birdParam := flag.String("bird", settingDefault.birdSocket, "socket file for bird, set either in parameter or environment variable BIRD_SOCKET")
|
||||||
bird6Param := flag.String("bird6", settingDefault.bird6Socket, "socket file for bird6, set either in parameter or environment variable BIRD6_SOCKET")
|
|
||||||
listenParam := flag.String("listen", settingDefault.listen, "listen address, set either in parameter or environment variable BIRDLG_LISTEN")
|
listenParam := flag.String("listen", settingDefault.listen, "listen address, set either in parameter or environment variable BIRDLG_LISTEN")
|
||||||
AllowedIPsParam := flag.String("allowed", strings.Join(settingDefault.allowedIPs, ","), "IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs.")
|
AllowedIPsParam := flag.String("allowed", strings.Join(settingDefault.allowedIPs, ","), "IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs.")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
setting.birdSocket = *birdParam
|
setting.birdSocket = *birdParam
|
||||||
setting.bird6Socket = *bird6Param
|
|
||||||
setting.listen = *listenParam
|
setting.listen = *listenParam
|
||||||
setting.allowedIPs = strings.Split(*AllowedIPsParam, ",")
|
setting.allowedIPs = strings.Split(*AllowedIPsParam, ",")
|
||||||
|
|
||||||
// Start HTTP server
|
// Start HTTP server
|
||||||
http.HandleFunc("/", invalidHandler)
|
http.HandleFunc("/", invalidHandler)
|
||||||
http.HandleFunc("/bird", birdHandler)
|
http.HandleFunc("/bird", birdHandler)
|
||||||
http.HandleFunc("/bird6", bird6Handler)
|
http.HandleFunc("/bird6", birdHandler)
|
||||||
http.HandleFunc("/traceroute", tracerouteIPv4Wrapper)
|
http.HandleFunc("/traceroute", tracerouteHandler)
|
||||||
http.HandleFunc("/traceroute6", tracerouteIPv6Wrapper)
|
http.HandleFunc("/traceroute6", tracerouteHandler)
|
||||||
http.ListenAndServe(*listenParam, handlers.LoggingHandler(os.Stdout, accessHandler(http.DefaultServeMux)))
|
http.ListenAndServe(*listenParam, handlers.LoggingHandler(os.Stdout, accessHandler(http.DefaultServeMux)))
|
||||||
}
|
}
|
||||||
|
|||||||
64
proxy/template.Dockerfile
Normal file
64
proxy/template.Dockerfile
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
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 . .
|
||||||
|
RUN go build -ldflags "-w -s" -o /proxy
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
#if defined(ARCH_AMD64)
|
||||||
|
FROM amd64/debian:sid AS step_1
|
||||||
|
ENV TARGET_ARCH=x86_64
|
||||||
|
#elif defined(ARCH_I386)
|
||||||
|
FROM i386/debian:sid AS step_1
|
||||||
|
ENV TARGET_ARCH=i386
|
||||||
|
#elif defined(ARCH_ARM32V7)
|
||||||
|
FROM arm32v7/debian:sid AS step_1
|
||||||
|
ENV TARGET_ARCH=arm
|
||||||
|
#elif defined(ARCH_ARM64V8)
|
||||||
|
FROM arm64v8/debian:sid AS step_1
|
||||||
|
ENV TARGET_ARCH=aarch64
|
||||||
|
#elif defined(ARCH_PPC64LE)
|
||||||
|
FROM ppc64le/debian:sid AS step_1
|
||||||
|
ENV TARGET_ARCH=ppc64le
|
||||||
|
#elif defined(ARCH_S390X)
|
||||||
|
FROM s390x/debian:sid AS step_1
|
||||||
|
ENV TARGET_ARCH=s390
|
||||||
|
#else
|
||||||
|
#error "Architecture not set"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y \
|
||||||
|
build-essential musl-dev musl-tools tar wget git
|
||||||
|
RUN git clone https://github.com/sabotage-linux/kernel-headers.git
|
||||||
|
RUN wget https://sourceforge.net/projects/traceroute/files/traceroute/traceroute-2.1.0/traceroute-2.1.0.tar.gz/download \
|
||||||
|
-O traceroute-2.1.0.tar.gz
|
||||||
|
RUN tar xvf traceroute-2.1.0.tar.gz \
|
||||||
|
&& cd traceroute-2.1.0 \
|
||||||
|
&& make -j4 CC=musl-gcc CFLAGS="-I/root/kernel-headers/${TARGET_ARCH}/include" LDFLAGS="-static"
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
FROM scratch AS step_2
|
||||||
|
ENV PATH=/
|
||||||
|
COPY --from=step_0 /proxy /
|
||||||
|
COPY --from=step_1 /root/traceroute-2.1.0/traceroute/traceroute /
|
||||||
|
ENTRYPOINT ["/proxy"]
|
||||||
@@ -4,20 +4,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/shlex"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wrapper of traceroute, IPv4
|
|
||||||
func tracerouteIPv4Wrapper(httpW http.ResponseWriter, httpR *http.Request) {
|
|
||||||
tracerouteRealHandler(false, httpW, httpR)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper of traceroute, IPv6
|
|
||||||
func tracerouteIPv6Wrapper(httpW http.ResponseWriter, httpR *http.Request) {
|
|
||||||
tracerouteRealHandler(true, httpW, httpR)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tracerouteTryExecute(cmd []string, args [][]string) ([]byte, string) {
|
func tracerouteTryExecute(cmd []string, args [][]string) ([]byte, string) {
|
||||||
var output []byte
|
var output []byte
|
||||||
var errString = ""
|
var errString = ""
|
||||||
@@ -35,97 +29,48 @@ func tracerouteTryExecute(cmd []string, args [][]string) ([]byte, string) {
|
|||||||
return nil, errString
|
return nil, errString
|
||||||
}
|
}
|
||||||
|
|
||||||
// Real handler of traceroute requests
|
func tracerouteHandler(httpW http.ResponseWriter, httpR *http.Request) {
|
||||||
func tracerouteRealHandler(useIPv6 bool, httpW http.ResponseWriter, httpR *http.Request) {
|
|
||||||
query := string(httpR.URL.Query().Get("q"))
|
query := string(httpR.URL.Query().Get("q"))
|
||||||
query = strings.TrimSpace(query)
|
query = strings.TrimSpace(query)
|
||||||
|
|
||||||
if query == "" {
|
if query == "" {
|
||||||
invalidHandler(httpW, httpR)
|
invalidHandler(httpW, httpR)
|
||||||
} else {
|
} else {
|
||||||
|
args, err := shlex.Split(query)
|
||||||
|
if err != nil {
|
||||||
|
httpW.WriteHeader(http.StatusInternalServerError)
|
||||||
|
httpW.Write([]byte(fmt.Sprintf("failed to parse args: %s\n", err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var result []byte
|
var result []byte
|
||||||
var errString string
|
var errString string
|
||||||
if runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" {
|
skippedCounter := 0
|
||||||
if useIPv6 {
|
|
||||||
result, errString = tracerouteTryExecute(
|
if runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" || runtime.GOOS == "openbsd" {
|
||||||
[]string{
|
result, errString = tracerouteTryExecute(
|
||||||
"traceroute6",
|
[]string{
|
||||||
"traceroute",
|
"traceroute",
|
||||||
},
|
"traceroute",
|
||||||
[][]string{
|
},
|
||||||
{"-q1", "-w1", query},
|
[][]string{
|
||||||
{"-q1", "-w1", query},
|
append([]string{"-q1", "-w1"}, args...),
|
||||||
},
|
args,
|
||||||
)
|
},
|
||||||
} else {
|
)
|
||||||
result, errString = tracerouteTryExecute(
|
|
||||||
[]string{
|
|
||||||
"traceroute",
|
|
||||||
"traceroute6",
|
|
||||||
},
|
|
||||||
[][]string{
|
|
||||||
{"-q1", "-w1", query},
|
|
||||||
{"-q1", "-w1", query},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if runtime.GOOS == "openbsd" {
|
|
||||||
if useIPv6 {
|
|
||||||
result, errString = tracerouteTryExecute(
|
|
||||||
[]string{
|
|
||||||
"traceroute6",
|
|
||||||
"traceroute",
|
|
||||||
},
|
|
||||||
[][]string{
|
|
||||||
{"-q1", "-w1", query},
|
|
||||||
{"-q1", "-w1", query},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
result, errString = tracerouteTryExecute(
|
|
||||||
[]string{
|
|
||||||
"traceroute",
|
|
||||||
"traceroute6",
|
|
||||||
},
|
|
||||||
[][]string{
|
|
||||||
{"-A", "-q1", "-w1", query},
|
|
||||||
{"-A", "-q1", "-w1", query},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if runtime.GOOS == "linux" {
|
} else if runtime.GOOS == "linux" {
|
||||||
if useIPv6 {
|
result, errString = tracerouteTryExecute(
|
||||||
result, errString = tracerouteTryExecute(
|
[]string{
|
||||||
[]string{
|
"traceroute",
|
||||||
"traceroute",
|
"traceroute",
|
||||||
"traceroute",
|
"traceroute",
|
||||||
"traceroute",
|
},
|
||||||
"traceroute",
|
[][]string{
|
||||||
},
|
append([]string{"-q1", "-N32", "-w1"}, args...),
|
||||||
[][]string{
|
append([]string{"-q1", "-w1"}, args...),
|
||||||
{"-6", "-q1", "-N32", "-w1", query},
|
args,
|
||||||
{"-4", "-q1", "-N32", "-w1", query},
|
},
|
||||||
// For Busybox traceroute which doesn't support simultaneous requests
|
)
|
||||||
{"-6", "-q1", "-w1", query},
|
|
||||||
{"-4", "-q1", "-w1", query},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
result, errString = tracerouteTryExecute(
|
|
||||||
[]string{
|
|
||||||
"traceroute",
|
|
||||||
"traceroute",
|
|
||||||
"traceroute",
|
|
||||||
"traceroute",
|
|
||||||
},
|
|
||||||
[][]string{
|
|
||||||
{"-4", "-q1", "-N32", "-w1", query},
|
|
||||||
{"-6", "-q1", "-N32", "-w1", query},
|
|
||||||
// For Busybox traceroute which doesn't support simultaneous requests
|
|
||||||
{"-4", "-q1", "-w1", query},
|
|
||||||
{"-6", "-q1", "-w1", query},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
httpW.WriteHeader(http.StatusInternalServerError)
|
httpW.WriteHeader(http.StatusInternalServerError)
|
||||||
httpW.Write([]byte("traceroute not supported on this node.\n"))
|
httpW.Write([]byte("traceroute not supported on this node.\n"))
|
||||||
@@ -133,10 +78,18 @@ func tracerouteRealHandler(useIPv6 bool, httpW http.ResponseWriter, httpR *http.
|
|||||||
}
|
}
|
||||||
if errString != "" {
|
if errString != "" {
|
||||||
httpW.WriteHeader(http.StatusInternalServerError)
|
httpW.WriteHeader(http.StatusInternalServerError)
|
||||||
httpW.Write([]byte("traceroute returned error:\n\n" + errString))
|
httpW.Write([]byte(errString))
|
||||||
}
|
}
|
||||||
if result != nil {
|
if result != nil {
|
||||||
httpW.Write(result)
|
errString = string(result)
|
||||||
|
errString = regexp.MustCompile(`(?m)^\s*(\d*)\s*\*\n`).ReplaceAllStringFunc(errString, func(w string) string {
|
||||||
|
skippedCounter++
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
httpW.Write([]byte(strings.TrimSpace(errString)))
|
||||||
|
if skippedCounter > 0 {
|
||||||
|
httpW.Write([]byte("\n\n" + strconv.Itoa(skippedCounter) + " hops not responding."))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user