Compare commits
3 Commits
burble.dn4
...
enhancemen
Author | SHA1 | Date | |
---|---|---|---|
600bafe08d | |||
66e63c66a1 | |||
166234fa89 |
55
.drone.yml
55
.drone.yml
@ -1,55 +0,0 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
|
||||
- name: build frontend
|
||||
image: golang
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
commands:
|
||||
- cd frontend
|
||||
- go vet
|
||||
- go build
|
||||
|
||||
- name: build proxy
|
||||
image: golang
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
commands:
|
||||
- cd proxy
|
||||
- go vet
|
||||
- go build
|
||||
|
||||
- name: upload artifacts
|
||||
image: git.burble.dn42/burble.dn42/drone-gitea-pkg-plugin:latest
|
||||
settings:
|
||||
token:
|
||||
from_secret: TOKEN
|
||||
version: RELEASE
|
||||
artifact: proxy
|
||||
filename: proxy/proxy
|
||||
package: bird-lg-go
|
||||
owner: burble.dn42
|
||||
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: git.burble.dn42
|
||||
repo: git.burble.dn42/burble.dn42/bird-lg-go
|
||||
dockerfile: frontend/Dockerfile
|
||||
context: frontend/
|
||||
tags: frontend
|
||||
username: burble
|
||||
password:
|
||||
from_secret: TOKEN
|
||||
storage_driver: vfs
|
||||
|
||||
---
|
||||
kind: secret
|
||||
name: TOKEN
|
||||
get:
|
||||
path: burble.dn42/kv/data/drone/git.burble.dn42
|
||||
name: artifact-token
|
16
.github/dependabot.yml
vendored
16
.github/dependabot.yml
vendored
@ -1,16 +0,0 @@
|
||||
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
|
108
.github/workflows/develop.yaml
vendored
108
.github/workflows/develop.yaml
vendored
@ -1,108 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
go-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v4
|
||||
|
||||
- name: Run frontend unit test
|
||||
run: |
|
||||
export GO111MODULE=on
|
||||
cd frontend
|
||||
go get -v -t -d ./...
|
||||
go test -v ./...
|
||||
cd ..
|
||||
|
||||
- name: Run proxy unit test
|
||||
run: |
|
||||
export GO111MODULE=on
|
||||
cd proxy
|
||||
go get -v -t -d ./...
|
||||
go test -v ./...
|
||||
cd ..
|
||||
|
||||
docker-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Test whois binary in frontend image
|
||||
run: |
|
||||
docker build -t local/frontend frontend/
|
||||
docker run --rm --net host --entrypoint whois local/frontend github.com || exit 1
|
||||
docker run --rm --net host --entrypoint whois local/frontend -h whois.ripe.net github.com || exit 1
|
||||
docker run --rm --net host --entrypoint whois local/frontend -h whois.ripe.net:43 github.com || exit 1
|
||||
|
||||
- name: Test traceroute binary in proxy image
|
||||
run: |
|
||||
docker build -t local/proxy proxy/
|
||||
docker run --rm --net host --entrypoint traceroute local/proxy 127.0.0.1 || exit 1
|
||||
docker run --rm --net host --entrypoint traceroute local/proxy ::1 || exit 1
|
||||
|
||||
docker-develop:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- go-test
|
||||
- docker-test
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build frontend docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: '{{defaultContext}}:frontend'
|
||||
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||
push: true
|
||||
tags: |
|
||||
xddxdd/bird-lg-go:develop
|
||||
xddxdd/bird-lg-go:develop-${{ github.sha }}
|
||||
ghcr.io/xddxdd/bird-lg-go:frontend-develop
|
||||
ghcr.io/xddxdd/bird-lg-go:frontend-develop-${{ github.sha }}
|
||||
|
||||
- name: Build proxy docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: '{{defaultContext}}:proxy'
|
||||
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||
push: true
|
||||
tags: |
|
||||
xddxdd/bird-lgproxy-go:develop
|
||||
xddxdd/bird-lgproxy-go:develop-${{ github.sha }}
|
||||
ghcr.io/xddxdd/bird-lg-go:proxy-develop
|
||||
ghcr.io/xddxdd/bird-lg-go:proxy-develop-${{ github.sha }}
|
87
.github/workflows/release.yaml
vendored
87
.github/workflows/release.yaml
vendored
@ -1,87 +0,0 @@
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
go-release:
|
||||
name: Release Go Binary
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goos: [linux, windows, darwin]
|
||||
goarch: ["386", amd64, "arm", arm64]
|
||||
exclude:
|
||||
- goarch: "386"
|
||||
goos: darwin
|
||||
- goarch: "arm"
|
||||
goos: darwin
|
||||
- goarch: "arm"
|
||||
goos: windows
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Release frontend
|
||||
uses: wangyoucao577/go-release-action@v1.40
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
project_path: "./frontend"
|
||||
binary_name: "bird-lg-go"
|
||||
|
||||
- name: Release proxy
|
||||
uses: wangyoucao577/go-release-action@v1.40
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
project_path: "./proxy"
|
||||
binary_name: "bird-lgproxy-go"
|
||||
|
||||
docker-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build frontend docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: '{{defaultContext}}:frontend'
|
||||
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||
push: true
|
||||
tags: |
|
||||
xddxdd/bird-lg-go:latest
|
||||
xddxdd/bird-lg-go:${{ github.event.release.tag_name }}
|
||||
ghcr.io/xddxdd/bird-lg-go:frontend
|
||||
ghcr.io/xddxdd/bird-lg-go:frontend-${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Build proxy docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: '{{defaultContext}}:proxy'
|
||||
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||
push: true
|
||||
tags: |
|
||||
xddxdd/bird-lgproxy-go:latest
|
||||
xddxdd/bird-lgproxy-go:${{ github.event.release.tag_name }}
|
||||
ghcr.io/xddxdd/bird-lg-go:proxy
|
||||
ghcr.io/xddxdd/bird-lg-go:proxy-${{ github.event.release.tag_name }}
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -20,7 +20,3 @@ proxy/proxy
|
||||
|
||||
# don't include generated bindata file
|
||||
frontend/bindata.go
|
||||
|
||||
# don't include generated Dockerfiles
|
||||
frontend/Dockerfile.*
|
||||
proxy/Dockerfile.*
|
46
.travis.yml
Normal file
46
.travis.yml
Normal file
@ -0,0 +1,46 @@
|
||||
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=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=ppc64le
|
||||
- PROGRAM=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=s390x
|
||||
- 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
|
||||
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=ppc64le
|
||||
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=s390x
|
||||
|
||||
addons:
|
||||
apt:
|
||||
update: true
|
||||
packages:
|
||||
- qemu-user-static
|
||||
- binfmt-support
|
||||
|
||||
install:
|
||||
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
|
||||
script:
|
||||
- |
|
||||
# Build image
|
||||
docker build \
|
||||
--build-arg IMAGE_ARCH=$IMAGE_ARCH \
|
||||
-t $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH \
|
||||
-f $PROGRAM/Dockerfile \
|
||||
$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
|
13
Makefile
13
Makefile
@ -1,13 +0,0 @@
|
||||
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/bird-lg-go
|
||||
install -m 755 proxy/proxy /usr/local/bin/bird-lgproxy-go
|
221
README.md
221
README.md
@ -1,45 +1,12 @@
|
||||
# Bird-lg-go
|
||||
|
||||
[](https://ci.burble.dn42/burble.dn42/bird-lg-go)
|
||||
Bird-lg-go
|
||||
==========
|
||||
|
||||
An alternative implementation for [bird-lg](https://github.com/sileht/bird-lg) written in Go. Both frontend and backend (proxy) are implemented, and can work with either the original Python implementation or the Go implementation.
|
||||
|
||||
> The code on master branch no longer support BIRDv1. Branch "bird1" is the last version that supports BIRDv1.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Bird-lg-go](#bird-lg-go)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Build Instructions](#build-instructions)
|
||||
- [Build Docker Images](#build-docker-images)
|
||||
- [Frontend](#frontend)
|
||||
- [Proxy](#proxy)
|
||||
- [Advanced Features](#advanced-features)
|
||||
- [Display names](#display-names)
|
||||
- [IP addresses](#ip-addresses)
|
||||
- [API](#api)
|
||||
- [Telegram Bot Webhook](#telegram-bot-webhook)
|
||||
- [Credits](#credits)
|
||||
- [License](#license)
|
||||
|
||||
## Build Instructions
|
||||
|
||||
You need to have **Go 1.16 or newer** installed on your machine.
|
||||
|
||||
Run `make` to build binaries for both the frontend and the proxy.
|
||||
|
||||
Optionally run `make install` to install them to `/usr/local/bin` (`bird-lg-go` and `bird-lgproxy-go`).
|
||||
|
||||
### Build Docker Images
|
||||
|
||||
Use the Dockerfiles in `frontend` and `proxy` directory.
|
||||
|
||||
Ready-to-use images are available at:
|
||||
|
||||
- Frontend: <https://hub.docker.com/r/xddxdd/bird-lg-go>
|
||||
- Proxy: <https://hub.docker.com/r/xddxdd/bird-lgproxy-go>
|
||||
|
||||
## Frontend
|
||||
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.
|
||||
|
||||
@ -51,61 +18,40 @@ Features implemented:
|
||||
- Work with both Python proxy (lgproxy.py) and Go proxy (proxy dir of this project)
|
||||
- Visualize AS paths as picture (bgpmap feature)
|
||||
|
||||
Configuration can be set in:
|
||||
Usage: all configuration is done via commandline parameters or environment variables, no config file.
|
||||
|
||||
- `bird-lg.[json/yaml/etc]` in current directory
|
||||
- `/etc/bird-lg/bird-lg.[json/yaml/etc]`
|
||||
- Commandline parameter
|
||||
- Environment variables
|
||||
|
||||
Configuration is handled by [viper](https://github.com/spf13/viper), any config format supported by it can be used.
|
||||
|
||||
| Config Key | Parameter | Environment Variable | Description |
|
||||
| ---------- | --------- | -------------------- | ----------- |
|
||||
| servers | --servers | BIRDLG_SERVERS | server name prefixes, separated by comma |
|
||||
| domain | --domain | BIRDLG_DOMAIN | server name domain suffixes |
|
||||
| listen | --listen | BIRDLG_LISTEN | address bird-lg is listening on (default "5000") |
|
||||
| proxy_port | --proxy-port | BIRDLG_PROXY_PORT | port bird-lgproxy is running on (default 8000) |
|
||||
| whois | --whois | BIRDLG_WHOIS | whois server for queries (default "whois.verisign-grs.com"). Start with "/" to spacify local whois binary("/usr/local/whois"). |
|
||||
| dns_interface | --dns-interface | BIRDLG_DNS_INTERFACE | dns zone to query ASN information (default "asn.cymru.com") |
|
||||
| bgpmap_info | --bgpmap-info | BIRDLG_BGPMAP_INFO | the infos displayed in bgpmap, separated by comma, start with `:` means allow multiline (default "asn,as-name,ASName,descr") |
|
||||
| title_brand | --title-brand | BIRDLG_TITLE_BRAND | prefix of page titles in browser tabs (default "Bird-lg Go") |
|
||||
| navbar_brand | --navbar-brand | BIRDLG_NAVBAR_BRAND | brand to show in the navigation bar (default "Bird-lg Go") |
|
||||
| navbar_brand_url | --navbar-brand-url | BIRDLG_NAVBAR_BRAND_URL | the url of the brand to show in the navigation bar (default "/") |
|
||||
| navbar_all_servers | --navbar-all-servers | BIRDLG_NAVBAR_ALL_SERVERS | the text of "All servers" button in the navigation bar (default "ALL Servers") |
|
||||
| navbar_all_url | --navbar-all-url | BIRDLG_NAVBAR_ALL_URL | the URL of "All servers" button (default "all") |
|
||||
| net_specific_mode | --net-specific-mode | BIRDLG_NET_SPECIFIC_MODE | apply network-specific changes for some networks, use "dn42" for BIRD in dn42 network |
|
||||
| protocol_filter | --protocol-filter | BIRDLG_PROTOCOL_FILTER | protocol types to show in summary tables (comma separated list); defaults to all if not set |
|
||||
| name_filter | --name-filter | BIRDLG_NAME_FILTER | protocol names to hide in summary tables (RE2 syntax); defaults to none if not set |
|
||||
| timeout | --time-out | BIRDLG_TIMEOUT | time before request timed out, in seconds; defaults to 120 if not set |
|
||||
|
||||
### Examples
|
||||
| Parameter | Environment Variable | Description |
|
||||
| --------- | -------------------- | ----------- |
|
||||
| --servers | BIRDLG_SERVERS | server name prefixes, separated by comma |
|
||||
| --domain | BIRDLG_DOMAIN | server name domain suffixes |
|
||||
| --listen | BIRDLG_LISTEN | address bird-lg is listening on (default ":5000") |
|
||||
| --proxy-port | BIRDLG_PROXY_PORT | port bird-lgproxy is running on (default 8000) |
|
||||
| --whois | BIRDLG_WHOIS | whois server for queries (default "whois.verisign-grs.com") |
|
||||
| --dns-interface | BIRDLG_DNS_INTERFACE | dns zone to query ASN information (default "asn.cymru.com") |
|
||||
| --title-brand | BIRDLG_TITLE_BRAND | prefix of page titles in browser tabs (default "Bird-lg Go") |
|
||||
| --navbar-brand | BIRDLG_NAVBAR_BRAND | brand to show in the navigation bar (default "Bird-lg Go") |
|
||||
|
||||
Example: the following command starts the frontend with 2 BIRD nodes, with domain name "gigsgigscloud.dn42.lantian.pub" and "hostdare.dn42.lantian.pub", and proxies are running on port 8000 on both nodes.
|
||||
|
||||
```bash
|
||||
./frontend --servers=gigsgigscloud,hostdare --domain=dn42.lantian.pub --proxy-port=8000
|
||||
```
|
||||
./frontend --servers=gigsgigscloud,hostdare --domain=dn42.lantian.pub --proxy-port=8000
|
||||
|
||||
Example: the following docker-compose.yml entry does the same as above, but by starting a Docker container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
bird-lg:
|
||||
# Use xddxdd/bird-lg-go:develop for the latest build from master branch
|
||||
image: xddxdd/bird-lg-go:latest
|
||||
container_name: bird-lg
|
||||
restart: always
|
||||
environment:
|
||||
- BIRDLG_SERVERS=gigsgigscloud,hostdare
|
||||
- BIRDLG_DOMAIN=dn42.lantian.pub
|
||||
ports:
|
||||
- "5000:5000"
|
||||
```
|
||||
services:
|
||||
bird-lg:
|
||||
image: xddxdd/bird-lg-go
|
||||
container_name: bird-lg
|
||||
restart: always
|
||||
environment:
|
||||
- BIRDLG_SERVERS=gigsgigscloud,hostdare
|
||||
- BIRDLG_DOMAIN=dn42.lantian.pub
|
||||
ports:
|
||||
- "5000:5000"
|
||||
|
||||
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.
|
||||
|
||||
@ -116,112 +62,43 @@ Features implemented:
|
||||
- Executing traceroute command on Linux, FreeBSD and OpenBSD
|
||||
- Source IP restriction
|
||||
|
||||
Configuration can be set in:
|
||||
Usage: all configuration is done via commandline parameters or environment variables, no config file.
|
||||
|
||||
- `bird-lgproxy.[json/yaml/etc]` in current directory
|
||||
- `/etc/bird-lg/bird-lgproxy.[json/yaml/etc]`
|
||||
- Commandline parameter
|
||||
- Environment variables
|
||||
|
||||
Configuration is handled by [viper](https://github.com/spf13/viper), any config format supported by it can be used.
|
||||
|
||||
| Config Key | Parameter | Environment Variable | Description |
|
||||
| ---------- | --------- | -------------------- | ----------- |
|
||||
| allowed_ips | --allowed | ALLOWED_IPS | IPs or networks allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") |
|
||||
| bird_socket | --bird | BIRD_SOCKET | socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl") |
|
||||
| listen | --listen | BIRDLG_PROXY_PORT | listen address, set either in parameter or environment variable BIRDLG_PROXY_PORT(default "8000") |
|
||||
| traceroute_bin | --traceroute_bin | BIRDLG_TRACEROUTE_BIN | traceroute binary file, set either in parameter or environment variable BIRDLG_TRACEROUTE_BIN |
|
||||
| traceroute_flags | --traceroute_flags | BIRDLG_TRACEROUTE_FLAGS | traceroute flags, supports multiple flags separated with space. |
|
||||
| traceroute_raw | --traceroute_raw | BIRDLG_TRACEROUTE_RAW | whether to display traceroute outputs raw (default false) |
|
||||
|
||||
### Traceroute Binary Autodetection
|
||||
|
||||
If `traceroute_bin` or `traceroute_flags` is not set, then on startup, the proxy will try to `traceroute 127.0.0.1` with different traceroute binaries and arguments, in order to use the most optimized setting available, while maintaining compatibility with multiple variants of traceroute binaries.
|
||||
|
||||
Traceroute binaries will be autodetected in the following order:
|
||||
|
||||
1. If `traceroute_bin` is set:
|
||||
1. `[traceroute_bin] -q1 -N32 -w1 127.0.0.1` (Corresponds to Traceroute on Debian)
|
||||
2. `[traceroute_bin] -q1 -w1 127.0.0.1` (Corresponds to Traceroute on FreeBSD)
|
||||
3. `[traceroute_bin] 127.0.0.1` (Corresponds to Busybox Traceroute)
|
||||
2. `mtr -w -c1 -Z1 -G1 -b 127.0.0.1` (MTR)
|
||||
3. `traceroute -q1 -N32 -w1 127.0.0.1` (Corresponds to Traceroute on Debian)
|
||||
4. `traceroute -q1 -w1 127.0.0.1` (Corresponds to Traceroute on FreeBSD)
|
||||
5. `traceroute 127.0.0.1` (Corresponds to Busybox Traceroute)
|
||||
|
||||
### Examples
|
||||
| Parameter | Environment Variable | Description |
|
||||
| --------- | -------------------- | ----------- |
|
||||
| --allowed | ALLOWED_IPS | IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") |
|
||||
| --bird | BIRD_SOCKET | socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl") |
|
||||
| --listen | BIRDLG_LISTEN | listen address, set either in parameter or environment variable BIRDLG_LISTEN (default ":8000") |
|
||||
|
||||
Example: start proxy with default configuration, should work "out of the box" on Debian 9 with BIRDv1:
|
||||
|
||||
```bash
|
||||
./proxy
|
||||
```
|
||||
./proxy
|
||||
|
||||
Example: start proxy with custom bird socket location:
|
||||
|
||||
```bash
|
||||
./proxy --bird /run/bird.ctl
|
||||
```
|
||||
./proxy --bird /run/bird.ctl
|
||||
|
||||
Example: the following docker-compose.yml entry does the same as above, but by starting a Docker container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
bird-lgproxy:
|
||||
# Use xddxdd/bird-lgproxy-go:develop for the latest build from master branch
|
||||
image: xddxdd/bird-lgproxy-go:latest
|
||||
container_name: bird-lgproxy
|
||||
restart: always
|
||||
volumes:
|
||||
- "/run/bird.ctl:/var/run/bird/bird.ctl"
|
||||
ports:
|
||||
- "192.168.0.1:8000:8000"
|
||||
```
|
||||
bird-lgproxy:
|
||||
image: xddxdd/bird-lgproxy-go
|
||||
container_name: bird-lgproxy
|
||||
restart: always
|
||||
volumes:
|
||||
- "/run/bird.ctl:/var/run/bird/bird.ctl"
|
||||
ports:
|
||||
- "192.168.0.1:8000:8000"
|
||||
|
||||
You can use source IP restriction to increase security. You should also bind the proxy to a specific interface and use an external firewall/iptables for added security.
|
||||
|
||||
## 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):
|
||||
|
||||
```bash
|
||||
./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:
|
||||
|
||||
```bash
|
||||
./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](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.
|
||||
|
||||
See [Telegram docs](docs/Telegram.md) for detailed information.
|
||||
|
||||
## Credits
|
||||
Credits
|
||||
-------
|
||||
|
||||
- Everyone who contributed to this project (see Contributors section on the right)
|
||||
- Mehdi Abaakouk for creating [the original bird-lg project](https://github.com/sileht/bird-lg)
|
||||
- [Bootstrap](https://getbootstrap.com/) as web UI framework
|
||||
|
||||
## License
|
||||
License
|
||||
-------
|
||||
|
||||
GPL 3.0
|
||||
|
202
docs/API.md
202
docs/API.md
@ -1,202 +0,0 @@
|
||||
# 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": [
|
||||
"alpha"
|
||||
],
|
||||
"type": "bird",
|
||||
"args": "show status"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "",
|
||||
"result": [
|
||||
{
|
||||
"server": "alpha",
|
||||
"data": "BIRD v2.0.7-137-g61dae32b\nRouter ID is 1.2.3.4\nCurrent server time is 2021-01-17 04:21:14.792\nLast reboot on 2021-01-03 08:15:48.494\nLast reconfiguration on 2021-01-17 00:49:10.573\nDaemon is up and running\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Example response of type `server_list`
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [],
|
||||
"type": "server_list",
|
||||
"args": ""
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "",
|
||||
"result": [
|
||||
{
|
||||
"server": "gigsgigscloud",
|
||||
"data": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
@ -1,22 +0,0 @@
|
||||
# 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
|
@ -1,33 +1,19 @@
|
||||
FROM golang AS step_0
|
||||
ENV CGO_ENABLED=0 GO111MODULE=on
|
||||
FROM golang:buster AS step_0
|
||||
#
|
||||
# IMAGE_ARCH is the binary format of the final output
|
||||
# BUILD_ARCH is the binaary format of the build host
|
||||
#
|
||||
ARG IMAGE_ARCH=amd64
|
||||
ARG BUILD_ARCH=amd64
|
||||
#
|
||||
ENV CGO_ENABLED=0 GOOS=linux GOARCH=$IMAGE_ARCH GO111MODULE=on
|
||||
WORKDIR /root
|
||||
COPY . .
|
||||
# go-bindata is run on the build host as part of the go generate step
|
||||
RUN GOARCH=$BUILD_ARCH go get -u github.com/kevinburke/go-bindata/...
|
||||
RUN go generate
|
||||
RUN go build -ldflags "-w -s" -o /frontend
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM alpine:edge AS step_1
|
||||
|
||||
WORKDIR /root
|
||||
RUN apk add --no-cache build-base pkgconf perl gettext \
|
||||
libidn2-dev libidn2-static libunistring-dev libunistring-static gnu-libiconv-dev
|
||||
|
||||
RUN wget https://github.com/rfc1036/whois/archive/refs/tags/v5.5.18.tar.gz \
|
||||
-O whois-5.5.18.tar.gz
|
||||
|
||||
RUN tar xvf whois-5.5.18.tar.gz \
|
||||
&& cd whois-5.5.18 \
|
||||
&& sed -i "s/#if defined _POSIX_C_SOURCE && _POSIX_C_SOURCE >= 200112L/#if 1/g" config.h \
|
||||
&& make whois -j4 \
|
||||
LDFLAGS="-static" CONFIG_FILE="/etc/whois.conf" PKG_CONFIG="pkg-config --static" HAVE_ICONV=1 \
|
||||
&& strip /root/whois-5.5.18/whois
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM scratch AS step_2
|
||||
ENV PATH=/
|
||||
ENV BIRDLG_WHOIS=/whois
|
||||
FROM scratch AS step_1
|
||||
COPY --from=step_0 /frontend /
|
||||
COPY --from=step_1 /root/whois-5.5.18/whois /
|
||||
COPY --from=step_1 /etc/services /etc/services
|
||||
ENTRYPOINT ["/frontend"]
|
||||
|
@ -1,3 +0,0 @@
|
||||
.PHONY: all
|
||||
all:
|
||||
go build -ldflags "-w -s" -o frontend
|
130
frontend/api.go
130
frontend/api.go
@ -1,130 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func TestApiServerListHandler(t *testing.T) {
|
||||
setting.servers = []string{"alpha", "beta", "gamma"}
|
||||
response := apiServerListHandler(apiRequest{})
|
||||
|
||||
assert.Equal(t, len(response.Result), 3)
|
||||
assert.Equal(t, response.Result[0].(apiGenericResultPair).Server, "alpha")
|
||||
assert.Equal(t, response.Result[1].(apiGenericResultPair).Server, "beta")
|
||||
assert.Equal(t, response.Result[2].(apiGenericResultPair).Server, "gamma")
|
||||
}
|
||||
|
||||
func TestApiGenericHandlerFactory(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, BirdSummaryData)
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
request := apiRequest{
|
||||
Servers: setting.servers,
|
||||
Type: "bird",
|
||||
Args: "show protocols",
|
||||
}
|
||||
|
||||
handler := apiGenericHandlerFactory("bird")
|
||||
response := handler(request)
|
||||
|
||||
assert.Equal(t, response.Error, "")
|
||||
|
||||
result := response.Result[0].(*apiGenericResultPair)
|
||||
assert.Equal(t, result.Server, "alpha")
|
||||
assert.Equal(t, result.Data, BirdSummaryData)
|
||||
}
|
||||
|
||||
func TestApiSummaryHandler(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, BirdSummaryData)
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
request := apiRequest{
|
||||
Servers: setting.servers,
|
||||
Type: "summary",
|
||||
Args: "",
|
||||
}
|
||||
response := apiSummaryHandler(request)
|
||||
|
||||
assert.Equal(t, response.Error, "")
|
||||
|
||||
summary := response.Result[0].(*apiSummaryResultPair)
|
||||
assert.Equal(t, summary.Server, "alpha")
|
||||
// Protocol list will be sorted
|
||||
assert.Equal(t, summary.Data[1].Name, "device1")
|
||||
assert.Equal(t, summary.Data[1].Proto, "Device")
|
||||
assert.Equal(t, summary.Data[1].Table, "---")
|
||||
assert.Equal(t, summary.Data[1].State, "up")
|
||||
assert.Equal(t, summary.Data[1].Since, "2021-08-27")
|
||||
assert.Equal(t, summary.Data[1].Info, "")
|
||||
}
|
||||
|
||||
func TestApiSummaryHandlerError(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock backend error")
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
request := apiRequest{
|
||||
Servers: setting.servers,
|
||||
Type: "summary",
|
||||
Args: "",
|
||||
}
|
||||
response := apiSummaryHandler(request)
|
||||
|
||||
assert.Equal(t, response.Error, "Mock backend error")
|
||||
}
|
||||
|
||||
func TestApiWhoisHandler(t *testing.T) {
|
||||
expectedData := "Mock Data"
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS6939",
|
||||
response: expectedData,
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
|
||||
request := apiRequest{
|
||||
Servers: []string{},
|
||||
Type: "",
|
||||
Args: "AS6939",
|
||||
}
|
||||
response := apiWhoisHandler(request)
|
||||
|
||||
assert.Equal(t, response.Error, "")
|
||||
|
||||
whoisResult := response.Result[0].(apiGenericResultPair)
|
||||
assert.Equal(t, whoisResult.Server, "")
|
||||
assert.Equal(t, whoisResult.Data, expectedData)
|
||||
}
|
||||
|
||||
func TestApiErrorHandler(t *testing.T) {
|
||||
err := errors.New("Mock Error")
|
||||
response := apiErrorHandler(err)
|
||||
assert.Equal(t, response.Error, "Mock Error")
|
||||
}
|
||||
|
||||
func TestApiHandler(t *testing.T) {
|
||||
setting.servers = []string{"alpha", "beta", "gamma"}
|
||||
|
||||
request := apiRequest{
|
||||
Servers: []string{},
|
||||
Type: "server_list",
|
||||
Args: "",
|
||||
}
|
||||
requestJson, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/api", bytes.NewReader(requestJson))
|
||||
w := httptest.NewRecorder()
|
||||
apiHandler(w, r)
|
||||
|
||||
var response apiResponse
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(response.Result), 3)
|
||||
// Hard to unmarshal JSON into apiGenericResultPair objects, won't check here
|
||||
}
|
||||
|
||||
func TestApiHandlerBadJSON(t *testing.T) {
|
||||
setting.servers = []string{"alpha", "beta", "gamma"}
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/api", strings.NewReader("{bad json}"))
|
||||
w := httptest.NewRecorder()
|
||||
apiHandler(w, r)
|
||||
|
||||
var response apiResponse
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(response.Result), 0)
|
||||
}
|
||||
|
||||
func TestApiHandlerInvalidType(t *testing.T) {
|
||||
setting.servers = []string{"alpha", "beta", "gamma"}
|
||||
|
||||
request := apiRequest{
|
||||
Servers: setting.servers,
|
||||
Type: "invalid_type",
|
||||
Args: "",
|
||||
}
|
||||
requestJson, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/api", bytes.NewReader(requestJson))
|
||||
w := httptest.NewRecorder()
|
||||
apiHandler(w, r)
|
||||
|
||||
var response apiResponse
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(response.Result), 0)
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ASNCache map[string]string
|
||||
|
||||
func (cache ASNCache) _lookup(asn string) string {
|
||||
// Try to get ASN representation using DNS
|
||||
if setting.dnsInterface != "" {
|
||||
records, err := net.LookupTXT(fmt.Sprintf("AS%s.%s", asn, setting.dnsInterface))
|
||||
if err == nil {
|
||||
result := strings.Join(records, " ")
|
||||
if resultSplit := strings.Split(result, " | "); len(resultSplit) > 1 {
|
||||
result = strings.Join(resultSplit[1:], "\n")
|
||||
}
|
||||
return fmt.Sprintf("AS%s\n%s", asn, result)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get ASN representation using WHOIS
|
||||
if setting.whoisServer != "" {
|
||||
if setting.bgpmapInfo == "" {
|
||||
setting.bgpmapInfo = "asn,as-name,ASName,descr"
|
||||
}
|
||||
records := whois(fmt.Sprintf("AS%s", asn))
|
||||
if records != "" {
|
||||
recordsSplit := strings.Split(records, "\n")
|
||||
var result []string
|
||||
for _, title := range strings.Split(setting.bgpmapInfo, ",") {
|
||||
if title == "asn" {
|
||||
result = append(result, "AS"+asn)
|
||||
}
|
||||
}
|
||||
for _, title := range strings.Split(setting.bgpmapInfo, ",") {
|
||||
allow_multiline := false
|
||||
if title[0] == ':' && len(title) >= 2 {
|
||||
title = title[1:]
|
||||
allow_multiline = true
|
||||
}
|
||||
for _, line := range recordsSplit {
|
||||
if len(line) == 0 || line[0] == '%' || !strings.Contains(line, ":") {
|
||||
continue
|
||||
}
|
||||
linearr := strings.SplitN(line, ":", 2)
|
||||
line_title := linearr[0]
|
||||
content := strings.TrimSpace(linearr[1])
|
||||
if line_title != title {
|
||||
continue
|
||||
}
|
||||
result = append(result, content)
|
||||
if !allow_multiline {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if len(result) > 0 {
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (cache ASNCache) Lookup(asn string) string {
|
||||
cachedValue, cacheOk := cache[asn]
|
||||
if cacheOk {
|
||||
return cachedValue
|
||||
}
|
||||
|
||||
result := cache._lookup(asn)
|
||||
if len(result) == 0 {
|
||||
result = fmt.Sprintf("AS%s", asn)
|
||||
}
|
||||
|
||||
cache[asn] = result
|
||||
return result
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func TestGetASNRepresentationDNS(t *testing.T) {
|
||||
checkNetwork(t)
|
||||
|
||||
setting.dnsInterface = "asn.cymru.com"
|
||||
setting.whoisServer = ""
|
||||
cache := make(ASNCache)
|
||||
result := cache.Lookup("6939")
|
||||
if !strings.Contains(result, "HURRICANE") {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetASNRepresentationDNSFallback(t *testing.T) {
|
||||
checkNetwork(t)
|
||||
|
||||
setting.dnsInterface = "invalid.example.com"
|
||||
setting.whoisServer = "whois.arin.net"
|
||||
cache := make(ASNCache)
|
||||
result := cache.Lookup("6939")
|
||||
if !strings.Contains(result, "HURRICANE") {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetASNRepresentationWhois(t *testing.T) {
|
||||
checkNetwork(t)
|
||||
|
||||
setting.dnsInterface = ""
|
||||
setting.whoisServer = "whois.arin.net"
|
||||
cache := make(ASNCache)
|
||||
result := cache.Lookup("6939")
|
||||
if !strings.Contains(result, "HURRICANE") {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetASNRepresentationFallback(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
setting.whoisServer = ""
|
||||
cache := make(ASNCache)
|
||||
result := cache.Lookup("6939")
|
||||
assert.Equal(t, result, "AS6939")
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,73 +0,0 @@
|
||||
// adapted from https://stackoverflow.com/a/57080195
|
||||
|
||||
document.querySelectorAll('table.sortable')
|
||||
.forEach((table)=> {
|
||||
table.querySelectorAll('th')
|
||||
.forEach((element, columnNo) => {
|
||||
element.addEventListener('click', event => {
|
||||
if(element.classList.contains('ascSorted')) {
|
||||
dir = -1;
|
||||
element.classList.remove('ascSorted');
|
||||
element.classList.add('descSorted');
|
||||
element.innerText = element.innerText.slice(0,-2) + " ↓";
|
||||
} else if(element.classList.contains('descSorted')) {
|
||||
dir = 1;
|
||||
element.classList.remove('descSorted');
|
||||
element.classList.add('ascSorted');
|
||||
element.innerText = element.innerText.slice(0,-2) + " ↑";
|
||||
} else {
|
||||
dir = 1;
|
||||
element.classList.add('ascSorted');
|
||||
element.innerText += " ↑";
|
||||
}
|
||||
sortTable(table, columnNo, 0, dir, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function sortTable(table, priCol, secCol, priDir, secDir) {
|
||||
const tableBody = table.querySelector('tbody');
|
||||
const tableData = table2data(tableBody);
|
||||
tableData.sort((a, b) => {
|
||||
if(a[priCol] === b[priCol]) {
|
||||
if(a[secCol] > b[secCol]) {
|
||||
return secDir;
|
||||
} else {
|
||||
return -secDir;
|
||||
}
|
||||
} else if(a[priCol] > b[priCol]) {
|
||||
return priDir;
|
||||
} else {
|
||||
return -priDir;
|
||||
}
|
||||
});
|
||||
data2table(tableBody, tableData);
|
||||
}
|
||||
|
||||
function table2data(tableBody) {
|
||||
const tableData = [];
|
||||
tableBody.querySelectorAll('tr')
|
||||
.forEach(row => {
|
||||
const rowData = [];
|
||||
row.querySelectorAll('td')
|
||||
.forEach(cell => {
|
||||
rowData.push(cell.innerHTML);
|
||||
});
|
||||
rowData.classList = row.classList.toString();
|
||||
tableData.push(rowData);
|
||||
});
|
||||
return tableData;
|
||||
}
|
||||
|
||||
function data2table(tableBody, tableData) {
|
||||
tableBody.querySelectorAll('tr')
|
||||
.forEach((row, i) => {
|
||||
const rowData = tableData[i];
|
||||
row.classList = rowData.classList;
|
||||
row.querySelectorAll('td')
|
||||
.forEach((cell, j) => {
|
||||
cell.innerHTML = rowData[j];
|
||||
});
|
||||
tableData.push(rowData);
|
||||
});
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<h2><span class="badge badge-info mr-3">BGPmap</span>{{ html .Target }}</h2>
|
||||
<div id="bgpmap">
|
||||
</div>
|
||||
|
||||
<script src="/static/jsdelivr/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script>
|
||||
<script src="/static/jsdelivr/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
var viz = new Viz();
|
||||
viz.renderSVGElement(atob({{ .Result }}))
|
||||
.then(element => {
|
||||
document.getElementById("bgpmap").appendChild(element);
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById("bgpmap").innerHTML = "<pre>"+error+"</pre>"
|
||||
});
|
||||
</script>
|
@ -1,2 +0,0 @@
|
||||
<h2 class="mb-3"><span class="badge badge-info mr-3">{{ html .ServerName }}</span>{{ html .Target }}</h2>
|
||||
{{ .Result }}
|
@ -1,107 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<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="/static/jsdelivr/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<style>
|
||||
.navwrap { flex-wrap: wrap; font-size: 0.9rem; }
|
||||
.logo {
|
||||
width: 180px;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="/"><img class="logo" src="/static/burble-dn42-64-white.png"></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">
|
||||
{{ if eq .AllServersURLCustom "all" }}
|
||||
<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}"
|
||||
href="/{{ $option }}/{{ .AllServersURL }}/{{ $target }}"> {{ .AllServerTitle }} </a>
|
||||
{{ else }}
|
||||
<a class="nav-link active"
|
||||
href="{{ .AllServersURLCustom }}"> {{ .AllServerTitle }} </a>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ $length := len .Servers }}
|
||||
{{ range $k, $v := .Servers }}
|
||||
<li class="nav-item">
|
||||
{{ if gt $length 1 }}
|
||||
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
|
||||
href="/{{ $option }}/{{ $v }}/{{ $target }}">{{ html (index $.ServersDisplay $k) }}</a>
|
||||
{{ else }}
|
||||
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
|
||||
href="/">{{ html (index $.ServersDisplay $k) }}</a>
|
||||
{{ end }}
|
||||
</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 | pathescape) }}">
|
||||
<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 mt-4">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
|
||||
<script src="/static/jsdelivr/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
||||
<script src="/static/jsdelivr/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
|
||||
<script src="/static/sortTable.js"></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>
|
@ -1,22 +0,0 @@
|
||||
{{ $ServerName := urlquery .ServerName }}
|
||||
|
||||
<table class="table table-striped table-bordered table-sm sortable">
|
||||
<thead>
|
||||
{{ range .Header }}
|
||||
{{ if ne . "Table" }}
|
||||
<th scope="col">{{ html . }}</th>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Rows }}
|
||||
<tr>
|
||||
<td><a href="/detail/{{ $ServerName }}/{{ urlquery .Name }}">{{ html .Name }}</a></td>
|
||||
<td>{{ html .Proto }}</td>
|
||||
<td><span class="badge badge-{{ .MappedState }}">{{ html .State }}</span></td>
|
||||
<td>{{ html .Since }}</td>
|
||||
<td>{{ html .Info }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
@ -1,2 +0,0 @@
|
||||
<h2><span class="badge badge-info mr-3">whois<span>{{ html .Target }}</h2>
|
||||
{{ .Result }}
|
@ -1,121 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The protocol name for each route (e.g. "ibgp_sea02") is encoded in the form:
|
||||
//
|
||||
// unicast [ibgp_sea02 2021-08-27 from fd86:bad:11b7:1::1] * (100/1015) [i]
|
||||
var protocolNameRe = regexp.MustCompile(`\[(.*?) .*\]`)
|
||||
|
||||
// Try to split the output into one chunk for each route.
|
||||
// Possible values are defined at https://gitlab.nic.cz/labs/bird/-/blob/v2.0.8/nest/rt-attr.c#L81-87
|
||||
var routeSplitRe = regexp.MustCompile("(unicast|blackhole|unreachable|prohibited)")
|
||||
|
||||
var routeViaRe = regexp.MustCompile(`(?m)^\t(via .*?)$`)
|
||||
var routeASPathRe = regexp.MustCompile(`(?m)^\tBGP\.as_path: (.*?)$`)
|
||||
|
||||
func makeEdgeAttrs(preferred bool) RouteAttrs {
|
||||
result := RouteAttrs{
|
||||
"fontsize": "12.0",
|
||||
func getASNRepresentation(asn string) string {
|
||||
records, err := net.LookupTXT(fmt.Sprintf("AS%s.%s", asn, setting.dnsInterface))
|
||||
if err != nil {
|
||||
// DNS query failed, only use ASN as output
|
||||
return fmt.Sprintf("AS%s", asn)
|
||||
}
|
||||
if preferred {
|
||||
result["color"] = "red"
|
||||
|
||||
result := strings.Join(records, " ")
|
||||
if resultSplit := strings.Split(result, " | "); len(resultSplit) > 1 {
|
||||
result = strings.Join(resultSplit[1:], "\\n")
|
||||
}
|
||||
return result
|
||||
return fmt.Sprintf("AS%s\\n%s", asn, result)
|
||||
}
|
||||
|
||||
func makePointAttrs(preferred bool) RouteAttrs {
|
||||
result := RouteAttrs{}
|
||||
if preferred {
|
||||
result["color"] = "red"
|
||||
func birdRouteToGraphviz(servers []string, responses []string, target string) string {
|
||||
graph := make(map[string]string)
|
||||
// Helper to add an edge
|
||||
addEdge := func(src string, dest string, attr string) {
|
||||
key := "\"" + src + "\" -> \"" + dest + "\""
|
||||
_, present := graph[key]
|
||||
// Do not remove edge's attributes if it's already present
|
||||
if present && len(attr) == 0 {
|
||||
return
|
||||
}
|
||||
graph[key] = attr
|
||||
}
|
||||
// Helper to set attribute for a point in graph
|
||||
addPoint := func(name string, attr string) {
|
||||
key := "\"" + name + "\""
|
||||
_, present := graph[key]
|
||||
// Do not remove point's attributes if it's already present
|
||||
if present && len(attr) == 0 {
|
||||
return
|
||||
}
|
||||
graph[key] = attr
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func birdRouteToGraph(servers []string, responses []string, target string) RouteGraph {
|
||||
graph := makeRouteGraph()
|
||||
|
||||
graph.AddPoint(target, false, RouteAttrs{"color": "red", "shape": "diamond"})
|
||||
|
||||
addPoint("Target: "+target, "[color=red,shape=diamond]")
|
||||
for serverID, server := range servers {
|
||||
response := responses[serverID]
|
||||
if len(response) == 0 {
|
||||
continue
|
||||
}
|
||||
graph.AddPoint(server, false, RouteAttrs{"color": "blue", "shape": "box"})
|
||||
routes := routeSplitRe.Split(response, -1)
|
||||
|
||||
addPoint(server, "[color=blue,shape=box]")
|
||||
// This is the best split point I can find for bird2
|
||||
routes := strings.Split(response, "\tvia ")
|
||||
routeFound := false
|
||||
for routeIndex, route := range routes {
|
||||
if routeIndex == 0 {
|
||||
var routeNexthop string
|
||||
var routeASPath string
|
||||
var routePreferred bool = routeIndex > 0 && strings.Contains(routes[routeIndex-1], "*")
|
||||
// Have to look at previous slice to determine if route is preferred, due to bad split point selection
|
||||
|
||||
for _, routeParameter := range strings.Split(route, "\n") {
|
||||
if strings.HasPrefix(routeParameter, "\tBGP.next_hop: ") {
|
||||
routeNexthop = strings.TrimPrefix(routeParameter, "\tBGP.next_hop: ")
|
||||
} else if strings.HasPrefix(routeParameter, "\tBGP.as_path: ") {
|
||||
routeASPath = strings.TrimPrefix(routeParameter, "\tBGP.as_path: ")
|
||||
}
|
||||
}
|
||||
if len(routeASPath) == 0 {
|
||||
// Either this is not a BGP route, or the information is incomplete
|
||||
continue
|
||||
}
|
||||
|
||||
var via string
|
||||
var paths []string
|
||||
var routePreferred bool = strings.Contains(route, "*")
|
||||
// Track non-BGP routes in the output by their protocol name, but draw them altogether in one line
|
||||
// so that there are no conflicts in the edge label
|
||||
var protocolName string
|
||||
// Connect each node on AS path
|
||||
paths := strings.Split(strings.TrimSpace(routeASPath), " ")
|
||||
|
||||
if match := routeViaRe.FindStringSubmatch(route); len(match) >= 2 {
|
||||
via = strings.TrimSpace(match[1])
|
||||
for pathIndex := range paths {
|
||||
paths[pathIndex] = strings.TrimPrefix(paths[pathIndex], "(")
|
||||
paths[pathIndex] = strings.TrimSuffix(paths[pathIndex], ")")
|
||||
}
|
||||
|
||||
if match := routeASPathRe.FindStringSubmatch(route); len(match) >= 2 {
|
||||
pathString := strings.TrimSpace(match[1])
|
||||
if len(pathString) > 0 {
|
||||
paths = strings.Split(strings.TrimSpace(match[1]), " ")
|
||||
for i := range paths {
|
||||
paths[i] = strings.TrimPrefix(paths[i], "(")
|
||||
paths[i] = strings.TrimSuffix(paths[i], ")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if match := protocolNameRe.FindStringSubmatch(route); len(match) >= 2 {
|
||||
protocolName = strings.TrimSpace(match[1])
|
||||
if routePreferred {
|
||||
protocolName = protocolName + "*"
|
||||
}
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
graph.AddEdge(server, target, strings.TrimSpace(protocolName+"\n"+via), makeEdgeAttrs(routePreferred))
|
||||
continue
|
||||
}
|
||||
|
||||
// Edges between AS
|
||||
for i := range paths {
|
||||
var src string
|
||||
var label string
|
||||
// Only show nexthop information on the first hop
|
||||
if i == 0 {
|
||||
src = server
|
||||
label = strings.TrimSpace(protocolName + "\n" + via)
|
||||
// First step starting from originating server
|
||||
if len(paths) > 0 {
|
||||
if len(routeNexthop) > 0 {
|
||||
// Edge from originating server to nexthop
|
||||
addEdge(server, "Nexthop:\\n"+routeNexthop, (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
// and from nexthop to AS
|
||||
addEdge("Nexthop:\\n"+routeNexthop, getASNRepresentation(paths[0]), (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
addPoint("Nexthop:\\n"+routeNexthop, "[shape=diamond]")
|
||||
routeFound = true
|
||||
} else {
|
||||
src = paths[i-1]
|
||||
label = ""
|
||||
// Edge from originating server to AS
|
||||
addEdge(server, getASNRepresentation(paths[0]), (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
routeFound = true
|
||||
}
|
||||
dst := paths[i]
|
||||
|
||||
graph.AddEdge(src, dst, label, makeEdgeAttrs(routePreferred))
|
||||
// Only set color for next step, origin color is set to blue above
|
||||
graph.AddPoint(dst, true, makePointAttrs(routePreferred))
|
||||
}
|
||||
|
||||
// Following steps, edges between AS
|
||||
for pathIndex := range paths {
|
||||
if pathIndex == 0 {
|
||||
continue
|
||||
}
|
||||
addEdge(getASNRepresentation(paths[pathIndex-1]), getASNRepresentation(paths[pathIndex]), (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
}
|
||||
// Last AS to destination
|
||||
src := paths[len(paths)-1]
|
||||
graph.AddEdge(src, target, "", makeEdgeAttrs(routePreferred))
|
||||
addEdge(getASNRepresentation(paths[len(paths)-1]), "Target: "+target, (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
}
|
||||
|
||||
if !routeFound {
|
||||
// Cannot find a path starting from this server
|
||||
addEdge(server, "Target: "+target, "[color=gray,label=\"?\"]")
|
||||
}
|
||||
}
|
||||
|
||||
return graph
|
||||
}
|
||||
|
||||
func birdRouteToGraphviz(servers []string, responses []string, targetName string) string {
|
||||
graph := birdRouteToGraph(servers, responses, targetName)
|
||||
return graph.ToGraphviz()
|
||||
// Combine all graphviz commands
|
||||
var result string
|
||||
for edge, attr := range graph {
|
||||
result += edge + " " + attr + ";\n"
|
||||
}
|
||||
return "digraph {\n" + result + "}\n"
|
||||
}
|
||||
|
@ -1,173 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RouteAttrs map[string]string
|
||||
|
||||
type RoutePoint struct {
|
||||
performLookup bool
|
||||
attrs RouteAttrs
|
||||
}
|
||||
|
||||
type RouteEdgeKey struct {
|
||||
src string
|
||||
dest string
|
||||
}
|
||||
|
||||
type RouteEdgeValue struct {
|
||||
label []string
|
||||
attrs RouteAttrs
|
||||
}
|
||||
|
||||
type RouteGraph struct {
|
||||
points map[string]RoutePoint
|
||||
edges map[RouteEdgeKey]RouteEdgeValue
|
||||
}
|
||||
|
||||
func makeRouteGraph() RouteGraph {
|
||||
return RouteGraph{
|
||||
points: make(map[string]RoutePoint),
|
||||
edges: make(map[RouteEdgeKey]RouteEdgeValue),
|
||||
}
|
||||
}
|
||||
|
||||
func makeRoutePoint() RoutePoint {
|
||||
return RoutePoint{
|
||||
performLookup: false,
|
||||
attrs: make(RouteAttrs),
|
||||
}
|
||||
}
|
||||
|
||||
func makeRouteEdgeValue() RouteEdgeValue {
|
||||
return RouteEdgeValue{
|
||||
label: []string{},
|
||||
attrs: make(RouteAttrs),
|
||||
}
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) attrsToString(attrs RouteAttrs) string {
|
||||
if len(attrs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
result := ""
|
||||
isFirst := true
|
||||
for k, v := range attrs {
|
||||
if isFirst {
|
||||
isFirst = false
|
||||
} else {
|
||||
result += ","
|
||||
}
|
||||
result += graph.escape(k) + "=" + graph.escape(v) + ""
|
||||
}
|
||||
|
||||
return "[" + result + "]"
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) escape(s string) string {
|
||||
result, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
} else {
|
||||
return string(result)
|
||||
}
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) AddEdge(src string, dest string, label string, attrs RouteAttrs) {
|
||||
// Add edges with same src/dest separately, multiple edges with same src/dest could exist
|
||||
edge := RouteEdgeKey{
|
||||
src: src,
|
||||
dest: dest,
|
||||
}
|
||||
|
||||
newValue, exists := graph.edges[edge]
|
||||
if !exists {
|
||||
newValue = makeRouteEdgeValue()
|
||||
}
|
||||
|
||||
if len(label) != 0 {
|
||||
newValue.label = append(newValue.label, label)
|
||||
}
|
||||
for k, v := range attrs {
|
||||
newValue.attrs[k] = v
|
||||
}
|
||||
|
||||
graph.edges[edge] = newValue
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) AddPoint(name string, performLookup bool, attrs RouteAttrs) {
|
||||
newValue, exists := graph.points[name]
|
||||
if !exists {
|
||||
newValue = makeRoutePoint()
|
||||
}
|
||||
|
||||
newValue.performLookup = performLookup
|
||||
for k, v := range attrs {
|
||||
newValue.attrs[k] = v
|
||||
}
|
||||
|
||||
graph.points[name] = newValue
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) GetEdge(src string, dest string) *RouteEdgeValue {
|
||||
key := RouteEdgeKey{
|
||||
src: src,
|
||||
dest: dest,
|
||||
}
|
||||
value, ok := graph.edges[key]
|
||||
if ok {
|
||||
return &value
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) GetPoint(name string) *RoutePoint {
|
||||
value, ok := graph.points[name]
|
||||
if ok {
|
||||
return &value
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) ToGraphviz() string {
|
||||
var result string
|
||||
|
||||
asnCache := make(ASNCache)
|
||||
|
||||
for name, value := range graph.points {
|
||||
var representation string
|
||||
|
||||
if value.performLookup {
|
||||
representation = asnCache.Lookup(name)
|
||||
} else {
|
||||
representation = name
|
||||
}
|
||||
|
||||
attrsCopy := value.attrs
|
||||
if attrsCopy == nil {
|
||||
attrsCopy = make(RouteAttrs)
|
||||
}
|
||||
attrsCopy["label"] = representation
|
||||
|
||||
result += fmt.Sprintf("%s %s;\n", graph.escape(name), graph.attrsToString(value.attrs))
|
||||
}
|
||||
|
||||
for key, value := range graph.edges {
|
||||
attrsCopy := value.attrs
|
||||
if attrsCopy == nil {
|
||||
attrsCopy = make(RouteAttrs)
|
||||
}
|
||||
if len(value.label) > 0 {
|
||||
attrsCopy["label"] = strings.Join(value.label, "\n")
|
||||
}
|
||||
result += fmt.Sprintf("%s -> %s %s;\n", graph.escape(key.src), graph.escape(key.dest), graph.attrsToString(attrsCopy))
|
||||
}
|
||||
|
||||
return "digraph {\n" + result + "}\n"
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func readDataFile(t *testing.T, filename string) string {
|
||||
_, sourceName, _, _ := runtime.Caller(0)
|
||||
projectRoot := path.Join(path.Dir(sourceName), "..")
|
||||
dir := path.Join(projectRoot, filename)
|
||||
|
||||
data, err := ioutil.ReadFile(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBirdRouteToGraph(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
|
||||
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
|
||||
result := birdRouteToGraph([]string{"node"}, []string{input}, "target")
|
||||
|
||||
// Source node must exist
|
||||
if result.GetPoint("node") == nil {
|
||||
t.Error("Result doesn't contain point node")
|
||||
}
|
||||
// Last hop must exist
|
||||
if result.GetPoint("4242423914") == nil {
|
||||
t.Error("Result doesn't contain point 4242423914")
|
||||
}
|
||||
// Destination must exist
|
||||
if result.GetPoint("target") == nil {
|
||||
t.Error("Result doesn't contain point target")
|
||||
}
|
||||
|
||||
// Verify that a few paths exist
|
||||
if result.GetEdge("node", "4242423914") == nil {
|
||||
t.Error("Result doesn't contain edge from node to 4242423914")
|
||||
}
|
||||
if result.GetEdge("node", "4242422688") == nil {
|
||||
t.Error("Result doesn't contain edge from node to 4242422688")
|
||||
}
|
||||
if result.GetEdge("4242422688", "4242423914") == nil {
|
||||
t.Error("Result doesn't contain edge from 4242422688 to 4242423914")
|
||||
}
|
||||
if result.GetEdge("4242423914", "target") == nil {
|
||||
t.Error("Result doesn't contain edge from 4242423914 to target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBirdRouteToGraphviz(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
|
||||
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
|
||||
result := birdRouteToGraphviz([]string{"node"}, []string{input}, "target")
|
||||
|
||||
if !strings.Contains(result, "digraph {") {
|
||||
t.Error("Response is not Graphviz data")
|
||||
}
|
||||
}
|
14
frontend/bindata/templates/bgpmap.tpl
Normal file
14
frontend/bindata/templates/bgpmap.tpl
Normal file
@ -0,0 +1,14 @@
|
||||
<h2>BGPmap: {{ html .Target }}</h2>
|
||||
|
||||
<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.body.appendChild(element);
|
||||
})
|
||||
.catch(error => {
|
||||
document.body.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 }}
|
68
frontend/bindata/templates/page.tpl
Normal file
68
frontend/bindata/templates/page.tpl
Normal file
@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="renderer" content="webkit">
|
||||
<title>{{ .Title }}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<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 := .Servers }}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
|
||||
href="/{{ $option }}/{{ $v }}/{{ $target }}">{{ $v }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ if .IsWhois }}
|
||||
{{ $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 $.URLOption }} selected{{end}}>{{ $v }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<input name="server" class="d-none" value="{{ $server }}">
|
||||
<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">
|
||||
{{ .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>
|
||||
</body>
|
||||
</html>
|
26
frontend/bindata/templates/summary.tpl
Normal file
26
frontend/bindata/templates/summary.tpl
Normal file
@ -0,0 +1,26 @@
|
||||
{{ $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>{{ .Proto }}</td>
|
||||
<td>{{ .Table }}</td>
|
||||
<td>{{ .State }}</td>
|
||||
<td>{{ .Since }}</td>
|
||||
<td>{{ .Info }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!--
|
||||
{{ .Raw }}
|
||||
-->
|
||||
|
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 }}
|
@ -49,53 +49,3 @@ func dn42WhoisFilter(whois string) string {
|
||||
return commandResult
|
||||
}
|
||||
}
|
||||
|
||||
/* experimental, behavior may change */
|
||||
func shortenWhoisFilter(whois string) string {
|
||||
commandResult := ""
|
||||
commandResultLonger := ""
|
||||
lines := 0
|
||||
linesLonger := 0
|
||||
skippedLines := 0
|
||||
skippedLinesLonger := 0
|
||||
|
||||
for _, s := range strings.Split(whois, "\n") {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
shouldSkip := false
|
||||
shouldSkip = shouldSkip || len(s) == 0
|
||||
shouldSkip = shouldSkip || len(s) > 0 && s[0] == '#'
|
||||
shouldSkip = shouldSkip || strings.Contains(strings.ToUpper(s), "REDACTED")
|
||||
|
||||
if shouldSkip {
|
||||
skippedLinesLonger++
|
||||
continue
|
||||
}
|
||||
|
||||
commandResultLonger += s + "\n"
|
||||
linesLonger++
|
||||
|
||||
shouldSkip = shouldSkip || len(s) > 80
|
||||
shouldSkip = shouldSkip || !strings.Contains(s, ":")
|
||||
shouldSkip = shouldSkip || strings.Index(s, ":") > 20
|
||||
|
||||
if shouldSkip {
|
||||
skippedLines++
|
||||
continue
|
||||
}
|
||||
|
||||
commandResult += s + "\n"
|
||||
lines++
|
||||
}
|
||||
|
||||
if lines < 5 {
|
||||
commandResult = commandResultLonger
|
||||
skippedLines = skippedLinesLonger
|
||||
}
|
||||
|
||||
if skippedLines > 0 {
|
||||
return commandResult + fmt.Sprintf("\n%d line(s) skipped.\n", skippedLines)
|
||||
} else {
|
||||
return commandResult
|
||||
}
|
||||
}
|
||||
|
@ -1,106 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortenWhoisFilterShorterMode(t *testing.T) {
|
||||
input := `
|
||||
Information line that will be removed
|
||||
|
||||
# Comment that will be removed
|
||||
Name: Redacted for privacy
|
||||
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
|
||||
Looooooooooooooooooooooong key: this line will be skipped.
|
||||
|
||||
Preserved1: this line isn't removed.
|
||||
Preserved2: this line isn't removed.
|
||||
Preserved3: this line isn't removed.
|
||||
Preserved4: this line isn't removed.
|
||||
Preserved5: this line isn't removed.
|
||||
|
||||
`
|
||||
|
||||
result := shortenWhoisFilter(input)
|
||||
|
||||
expectedResult := `Preserved1: this line isn't removed.
|
||||
Preserved2: this line isn't removed.
|
||||
Preserved3: this line isn't removed.
|
||||
Preserved4: this line isn't removed.
|
||||
Preserved5: this line isn't removed.
|
||||
|
||||
3 line(s) skipped.
|
||||
`
|
||||
|
||||
if result != expectedResult {
|
||||
t.Errorf("Output doesn't match expected: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortenWhoisFilterLongerMode(t *testing.T) {
|
||||
input := `
|
||||
Information line that will be removed
|
||||
|
||||
# Comment that will be removed
|
||||
Name: Redacted for privacy
|
||||
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
|
||||
Looooooooooooooooooooooong key: this line will be skipped.
|
||||
|
||||
Preserved1: this line isn't removed.
|
||||
|
||||
`
|
||||
|
||||
result := shortenWhoisFilter(input)
|
||||
|
||||
expectedResult := `Information line that will be removed
|
||||
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
|
||||
Looooooooooooooooooooooong key: this line will be skipped.
|
||||
Preserved1: this line isn't removed.
|
||||
|
||||
7 line(s) skipped.
|
||||
`
|
||||
|
||||
if result != expectedResult {
|
||||
t.Errorf("Output doesn't match expected: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortenWhoisFilterSkipNothing(t *testing.T) {
|
||||
input := `Preserved1: this line isn't removed.
|
||||
Preserved2: this line isn't removed.
|
||||
Preserved3: this line isn't removed.
|
||||
Preserved4: this line isn't removed.
|
||||
Preserved5: this line isn't removed.
|
||||
`
|
||||
|
||||
result := shortenWhoisFilter(input)
|
||||
|
||||
if result != input {
|
||||
t.Errorf("Output doesn't match expected: %s", result)
|
||||
}
|
||||
}
|
@ -1,29 +1,8 @@
|
||||
module github.com/xddxdd/bird-lg-go/frontend
|
||||
|
||||
go 1.17
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/jarcoal/httpmock v1.3.1
|
||||
github.com/magiconair/properties v1.8.7
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.16.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
1764
frontend/go.sum
1764
frontend/go.sum
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type channelData struct {
|
||||
@ -37,30 +35,15 @@ func batchRequest(servers []string, endpoint string, command string) []string {
|
||||
}(i)
|
||||
} else {
|
||||
// Compose URL and send the request
|
||||
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)
|
||||
url := "http://" + server + "." + setting.domain + ":" + strconv.Itoa(setting.proxyPort) + "/" + url.PathEscape(endpoint) + "?q=" + url.QueryEscape(command)
|
||||
go func(url string, i int) {
|
||||
client := http.Client{Timeout: time.Duration(setting.timeOut) * time.Second}
|
||||
response, err := client.Get(url)
|
||||
response, err := http.Get(url)
|
||||
if err != nil {
|
||||
ch <- channelData{i, "request failed: " + err.Error() + "\n"}
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 65536)
|
||||
n, err := io.ReadFull(response.Body, buf)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
ch <- channelData{i, "request failed: " + err.Error()}
|
||||
} else {
|
||||
ch <- channelData{i, string(buf[:n])}
|
||||
}
|
||||
text, _ := ioutil.ReadAll(response.Body)
|
||||
ch <- channelData{i, string(text)}
|
||||
}(url, i)
|
||||
}
|
||||
}
|
||||
|
@ -1,163 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
)
|
||||
|
||||
func TestBatchRequestIPv4(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock Result")
|
||||
httpmock.RegisterResponder("GET", "http://1.1.1.1:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://2.2.2.2:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://3.3.3.3:8000/mock?q=cmd", httpResponse)
|
||||
|
||||
setting.servers = []string{
|
||||
"1.1.1.1",
|
||||
"2.2.2.2",
|
||||
"3.3.3.3",
|
||||
}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
response := batchRequest(setting.servers, "mock", "cmd")
|
||||
|
||||
if len(response) != 3 {
|
||||
t.Error("Did not get response of all three mock servers")
|
||||
}
|
||||
for i := 0; i < len(response); i++ {
|
||||
if response[i] != "Mock Result" {
|
||||
t.Error("HTTP response mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchRequestIPv6(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock Result")
|
||||
httpmock.RegisterResponder("GET", "http://[2001:db8::1]:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://[2001:db8::2]:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://[2001:db8::3]:8000/mock?q=cmd", httpResponse)
|
||||
|
||||
setting.servers = []string{
|
||||
"2001:db8::1",
|
||||
"2001:db8::2",
|
||||
"2001:db8::3",
|
||||
}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
response := batchRequest(setting.servers, "mock", "cmd")
|
||||
|
||||
if len(response) != 3 {
|
||||
t.Error("Did not get response of all three mock servers")
|
||||
}
|
||||
for i := 0; i < len(response); i++ {
|
||||
if response[i] != "Mock Result" {
|
||||
t.Error("HTTP response mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchRequestEmptyResponse(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "")
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://beta:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://gamma:8000/mock?q=cmd", httpResponse)
|
||||
|
||||
setting.servers = []string{
|
||||
"alpha",
|
||||
"beta",
|
||||
"gamma",
|
||||
}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
response := batchRequest(setting.servers, "mock", "cmd")
|
||||
|
||||
if len(response) != 3 {
|
||||
t.Error("Did not get response of all three mock servers")
|
||||
}
|
||||
for i := 0; i < len(response); i++ {
|
||||
if !strings.Contains(response[i], "node returned empty response") {
|
||||
t.Error("Did not produce error for empty response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchRequestDomainSuffix(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock Result")
|
||||
httpmock.RegisterResponder("GET", "http://alpha.suffix:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://beta.suffix:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://gamma.suffix:8000/mock?q=cmd", httpResponse)
|
||||
|
||||
setting.servers = []string{
|
||||
"alpha",
|
||||
"beta",
|
||||
"gamma",
|
||||
}
|
||||
setting.domain = "suffix"
|
||||
setting.proxyPort = 8000
|
||||
response := batchRequest(setting.servers, "mock", "cmd")
|
||||
|
||||
if len(response) != 3 {
|
||||
t.Error("Did not get response of all three mock servers")
|
||||
}
|
||||
for i := 0; i < len(response); i++ {
|
||||
if response[i] != "Mock Result" {
|
||||
t.Error("HTTP response mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchRequestHTTPError(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpError := httpmock.NewErrorResponder(errors.New("Oops!"))
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpError)
|
||||
httpmock.RegisterResponder("GET", "http://beta:8000/mock?q=cmd", httpError)
|
||||
httpmock.RegisterResponder("GET", "http://gamma:8000/mock?q=cmd", httpError)
|
||||
|
||||
setting.servers = []string{
|
||||
"alpha",
|
||||
"beta",
|
||||
"gamma",
|
||||
}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
response := batchRequest(setting.servers, "mock", "cmd")
|
||||
|
||||
if len(response) != 3 {
|
||||
t.Error("Did not get response of all three mock servers")
|
||||
}
|
||||
for i := 0; i < len(response); i++ {
|
||||
if !strings.Contains(response[i], "request failed") {
|
||||
t.Error("Did not produce HTTP error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchRequestInvalidServer(t *testing.T) {
|
||||
setting.servers = []string{}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
response := batchRequest([]string{"invalid"}, "mock", "cmd")
|
||||
|
||||
if len(response) != 1 {
|
||||
t.Error("Did not get response of all mock servers")
|
||||
}
|
||||
if !strings.Contains(response[0], "invalid server") {
|
||||
t.Error("Did not produce invalid server error")
|
||||
}
|
||||
}
|
@ -1,14 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"flag"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// binary data
|
||||
//go:generate go-bindata -prefix bindata -o bindata.go bindata/...
|
||||
|
||||
type settingType struct {
|
||||
servers []string
|
||||
serversDisplay []string
|
||||
domain string
|
||||
proxyPort int
|
||||
whoisServer string
|
||||
@ -17,40 +20,82 @@ type settingType struct {
|
||||
netSpecificMode string
|
||||
titleBrand string
|
||||
navBarBrand string
|
||||
navBarBrandURL string
|
||||
navBarAllServer string
|
||||
navBarAllURL string
|
||||
bgpmapInfo string
|
||||
telegramBotName string
|
||||
protocolFilter []string
|
||||
nameFilter string
|
||||
timeOut int
|
||||
}
|
||||
|
||||
var setting settingType
|
||||
|
||||
func main() {
|
||||
parseSettings()
|
||||
ImportTemplates()
|
||||
var settingDefault = settingType{
|
||||
servers: []string{""},
|
||||
proxyPort: 8000,
|
||||
whoisServer: "whois.verisign-grs.com",
|
||||
listen: ":5000",
|
||||
dnsInterface: "asn.cymru.com",
|
||||
titleBrand: "Bird-lg Go",
|
||||
navBarBrand: "Bird-lg Go",
|
||||
}
|
||||
|
||||
var l net.Listener
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(setting.listen, "/") {
|
||||
// Delete existing socket file, ignore errors (will fail later anyway)
|
||||
os.Remove(setting.listen)
|
||||
l, err = net.Listen("unix", setting.listen)
|
||||
} else {
|
||||
listenAddr := setting.listen
|
||||
if !strings.Contains(listenAddr, ":") {
|
||||
listenAddr = ":" + listenAddr
|
||||
if env := os.Getenv("BIRDLG_SERVERS"); env != "" {
|
||||
settingDefault.servers = strings.Split(env, ",")
|
||||
}
|
||||
if env := os.Getenv("BIRDLG_DOMAIN"); env != "" {
|
||||
settingDefault.domain = env
|
||||
}
|
||||
if env := os.Getenv("BIRDLG_PROXY_PORT"); env != "" {
|
||||
var err error
|
||||
if settingDefault.proxyPort, err = strconv.Atoi(env); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l, err = net.Listen("tcp", listenAddr)
|
||||
}
|
||||
if env := os.Getenv("BIRDLG_WHOIS"); env != "" {
|
||||
settingDefault.whoisServer = env
|
||||
}
|
||||
if env := os.Getenv("BIRDLG_LISTEN"); env != "" {
|
||||
settingDefault.listen = env
|
||||
}
|
||||
if env := os.Getenv("BIRDLG_DNS_INTERFACE"); env != "" {
|
||||
settingDefault.dnsInterface = env
|
||||
}
|
||||
if env := os.Getenv("BIRDLG_NET_SPECIFIC_MODE"); env != "" {
|
||||
settingDefault.netSpecificMode = env
|
||||
}
|
||||
if env := os.Getenv("BIRDLG_TITLE_BRAND"); env != "" {
|
||||
settingDefault.titleBrand = env
|
||||
settingDefault.navBarBrand = env
|
||||
}
|
||||
if env := os.Getenv("BIRDLG_NAVBAR_BRAND"); env != "" {
|
||||
settingDefault.navBarBrand = env
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
serversPtr := flag.String("servers", strings.Join(settingDefault.servers, ","), "server name prefixes, separated by comma")
|
||||
domainPtr := flag.String("domain", settingDefault.domain, "server name domain suffixes")
|
||||
proxyPortPtr := flag.Int("proxy-port", settingDefault.proxyPort, "port bird-lgproxy is running on")
|
||||
whoisPtr := flag.String("whois", settingDefault.whoisServer, "whois server for queries")
|
||||
listenPtr := flag.String("listen", settingDefault.listen, "address bird-lg is listening on")
|
||||
dnsInterfacePtr := flag.String("dns-interface", settingDefault.dnsInterface, "dns zone to query ASN information")
|
||||
netSpecificModePtr := flag.String("net-specific-mode", settingDefault.netSpecificMode, "network specific operation mode, [(none)|dn42]")
|
||||
titleBrandPtr := flag.String("title-brand", settingDefault.titleBrand, "prefix of page titles in browser tabs")
|
||||
navBarBrandPtr := flag.String("navbar-brand", settingDefault.navBarBrand, "brand to show in the navigation bar")
|
||||
flag.Parse()
|
||||
|
||||
if *serversPtr == "" {
|
||||
panic("no server set")
|
||||
} else if *domainPtr == "" {
|
||||
panic("no base domain set")
|
||||
}
|
||||
|
||||
webServerStart(l)
|
||||
setting = settingType{
|
||||
strings.Split(*serversPtr, ","),
|
||||
*domainPtr,
|
||||
*proxyPortPtr,
|
||||
*whoisPtr,
|
||||
*listenPtr,
|
||||
*dnsInterfacePtr,
|
||||
strings.ToLower(*netSpecificModePtr),
|
||||
*titleBrandPtr,
|
||||
*navBarBrandPtr,
|
||||
}
|
||||
|
||||
ImportTemplates()
|
||||
webServerStart()
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
NETWORK_UNKNOWN = 0
|
||||
NETWORK_DOWN = 1
|
||||
NETWORK_UP = 2
|
||||
)
|
||||
|
||||
var networkState int = NETWORK_UNKNOWN
|
||||
func checkNetwork(t *testing.T) {
|
||||
if networkState == NETWORK_UNKNOWN {
|
||||
conn, err := net.DialTimeout("tcp", "8.8.8.8:53", 1*time.Second)
|
||||
if err != nil {
|
||||
networkState = NETWORK_DOWN
|
||||
} else {
|
||||
networkState = NETWORK_UP
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if networkState == NETWORK_DOWN {
|
||||
t.Skipf("Test skipped for network error")
|
||||
}
|
||||
}
|
@ -2,9 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
@ -13,26 +11,18 @@ import (
|
||||
|
||||
// static options map
|
||||
var optionsMap = map[string]string{
|
||||
"summary": "show protocols",
|
||||
"detail": "show protocols all ...",
|
||||
"route_from_protocol": "show route protocol ...",
|
||||
"route_from_protocol_all": "show route protocol ... all",
|
||||
"route_from_protocol_all_primary": "show route protocol ... all primary",
|
||||
"route_filtered_from_protocol": "show route filtered protocol ...",
|
||||
"route_filtered_from_protocol_all": "show route filtered protocol ... all",
|
||||
"route_from_origin": "show route where bgp_path.last = ...",
|
||||
"route_from_origin_all": "show route where bgp_path.last = ... all",
|
||||
"route_from_origin_all_primary": "show route where bgp_path.last = ... all primary",
|
||||
"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 ...",
|
||||
"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
|
||||
@ -45,7 +35,7 @@ var summaryStateMap = map[string]string{
|
||||
}
|
||||
|
||||
// render the page template
|
||||
func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, content template.HTML) {
|
||||
func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, content string) {
|
||||
path := r.URL.Path[1:]
|
||||
split := strings.SplitN(path, "/", 3)
|
||||
|
||||
@ -65,11 +55,8 @@ func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, co
|
||||
args := TemplatePage{
|
||||
Options: optionsMap,
|
||||
Servers: setting.servers,
|
||||
ServersDisplay: setting.serversDisplay,
|
||||
AllServersLinkActive: strings.EqualFold(split[1], strings.Join(setting.servers, "+")),
|
||||
AllServersLinkActive: strings.ToLower(split[1]) == strings.ToLower(strings.Join(setting.servers, "+")),
|
||||
AllServersURL: strings.Join(setting.servers, "+"),
|
||||
AllServerTitle: setting.navBarAllServer,
|
||||
AllServersURLCustom: setting.navBarAllURL,
|
||||
IsWhois: isWhois,
|
||||
WhoisTarget: whoisTarget,
|
||||
|
||||
@ -78,7 +65,6 @@ func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, co
|
||||
URLCommand: split[2],
|
||||
Title: setting.titleBrand + title,
|
||||
Brand: setting.navBarBrand,
|
||||
BrandURL: setting.navBarBrandURL,
|
||||
Content: content,
|
||||
}
|
||||
|
||||
@ -92,10 +78,9 @@ func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, co
|
||||
|
||||
// Write the given text to http response, and add whois links for
|
||||
// ASNs and IP addresses
|
||||
func smartFormatter(s string) template.HTML {
|
||||
func smartFormatter(s string) string {
|
||||
var result string
|
||||
result += "<pre>"
|
||||
s = template.HTMLEscapeString(s)
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
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:") {
|
||||
@ -109,20 +94,21 @@ func smartFormatter(s string) template.HTML {
|
||||
result += lineFormatted + "\n"
|
||||
}
|
||||
result += "</pre>"
|
||||
return template.HTML(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Parse bird show protocols result
|
||||
func summaryParse(data string, serverName string) (TemplateSummary, error) {
|
||||
args := TemplateSummary{
|
||||
ServerName: serverName,
|
||||
Raw: data,
|
||||
}
|
||||
// Output a table for the summary page
|
||||
func summaryTable(data string, serverName string) string {
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(data), "\n")
|
||||
if len(lines) <= 1 {
|
||||
// Likely backend returned an error message
|
||||
return args, errors.New(strings.TrimSpace(data))
|
||||
return "<pre>" + strings.TrimSpace(data) + "</pre>"
|
||||
}
|
||||
|
||||
args := TemplateSummary{
|
||||
ServerName: serverName,
|
||||
Raw: data,
|
||||
}
|
||||
|
||||
// extract the table header
|
||||
@ -134,9 +120,6 @@ func summaryParse(data string, serverName string) (TemplateSummary, error) {
|
||||
args.Header = append(args.Header, col)
|
||||
}
|
||||
|
||||
// Build regexp for nameFilter
|
||||
nameFilterRegexp := regexp.MustCompile(setting.nameFilter)
|
||||
|
||||
// sort the remaining rows
|
||||
rows := lines[1:]
|
||||
sort.Strings(rows)
|
||||
@ -160,24 +143,9 @@ func summaryParse(data string, serverName string) (TemplateSummary, error) {
|
||||
|
||||
if len(lineSplitted) >= 2 {
|
||||
row.Name = strings.TrimSpace(lineSplitted[1])
|
||||
if setting.nameFilter != "" && nameFilterRegexp.MatchString(row.Name) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(lineSplitted) >= 4 {
|
||||
row.Proto = strings.TrimSpace(lineSplitted[3])
|
||||
// Filter away unwanted protocol types, if setting.protocolFilter is non-empty
|
||||
found := false
|
||||
for _, protocol := range setting.protocolFilter {
|
||||
if strings.EqualFold(row.Proto, protocol) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(setting.protocolFilter) > 0 && !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(lineSplitted) >= 6 {
|
||||
row.Table = strings.TrimSpace(lineSplitted[5])
|
||||
@ -197,24 +165,13 @@ func summaryParse(data string, serverName string) (TemplateSummary, error) {
|
||||
args.Rows = append(args.Rows, row)
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Output a table for the summary page
|
||||
func summaryTable(data string, serverName string) template.HTML {
|
||||
result, err := summaryParse(data, serverName)
|
||||
|
||||
if err != nil {
|
||||
return template.HTML("<pre>" + template.HTMLEscapeString(err.Error()) + "</pre>")
|
||||
}
|
||||
|
||||
// render the summary template
|
||||
// finally, render the summary template
|
||||
tmpl := TemplateLibrary["summary"]
|
||||
var buffer bytes.Buffer
|
||||
err = tmpl.Execute(&buffer, result)
|
||||
err := tmpl.Execute(&buffer, args)
|
||||
if err != nil {
|
||||
fmt.Println("Error rendering summary:", err.Error())
|
||||
}
|
||||
|
||||
return template.HTML(buffer.String())
|
||||
return buffer.String()
|
||||
}
|
||||
|
@ -1,158 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const BirdSummaryData = `BIRD 2.0.8 ready.
|
||||
Name Proto Table State Since Info
|
||||
static1 Static master4 up 2021-08-27
|
||||
static2 Static master6 up 2021-08-27
|
||||
device1 Device --- up 2021-08-27
|
||||
kernel1 Kernel master6 up 2021-08-27
|
||||
kernel2 Kernel master4 up 2021-08-27
|
||||
direct1 Direct --- up 2021-08-27
|
||||
int_babel Babel --- up 2021-08-27
|
||||
`
|
||||
|
||||
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, template.HTML(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/"+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)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/xddxdd/bird-lg-go/issues/57
|
||||
func TestRenderPageTemplateXSS_2(t *testing.T) {
|
||||
initSettings()
|
||||
|
||||
evil := "<script>alert('evil');</script>"
|
||||
|
||||
r := httptest.NewRequest("GET", "/generic/dummy_server/"+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 := string(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 := string(summaryTable(evilData, evil))
|
||||
|
||||
if strings.Contains(result, evil) {
|
||||
t.Errorf("XSS injection succeeded: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryTableProtocolFilter(t *testing.T) {
|
||||
initSettings()
|
||||
setting.protocolFilter = []string{"Static", "Direct", "Babel"}
|
||||
|
||||
result := string(summaryTable(BirdSummaryData, "testserver"))
|
||||
expectedInclude := []string{"static1", "static2", "int_babel", "direct1"}
|
||||
expectedExclude := []string{"device1", "kernel1", "kernel2"}
|
||||
|
||||
for _, item := range expectedInclude {
|
||||
if !strings.Contains(result, item) {
|
||||
t.Errorf("Did not find expected %s in summary table output", result)
|
||||
}
|
||||
}
|
||||
for _, item := range expectedExclude {
|
||||
if strings.Contains(result, item) {
|
||||
t.Errorf("Found unexpected %s in summary table output", result)
|
||||
}
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
setting.protocolFilter = []string{}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSummaryTableNameFilter(t *testing.T) {
|
||||
initSettings()
|
||||
setting.nameFilter = "^static"
|
||||
|
||||
result := string(summaryTable(BirdSummaryData, "testserver"))
|
||||
expectedInclude := []string{"device1", "kernel1", "kernel2", "direct1", "int_babel"}
|
||||
expectedExclude := []string{"static1", "static2"}
|
||||
|
||||
for _, item := range expectedInclude {
|
||||
if !strings.Contains(result, item) {
|
||||
t.Errorf("Did not find expected %s in summary table output", result)
|
||||
}
|
||||
}
|
||||
for _, item := range expectedExclude {
|
||||
if strings.Contains(result, item) {
|
||||
t.Errorf("Found unexpected %s in summary table output", result)
|
||||
}
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
setting.nameFilter = ""
|
||||
})
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type viperSettingType struct {
|
||||
Servers string `mapstructure:"servers"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
ProxyPort int `mapstructure:"proxy_port"`
|
||||
WhoisServer string `mapstructure:"whois"`
|
||||
Listen string `mapstructure:"listen"`
|
||||
DNSInterface string `mapstructure:"dns_interface"`
|
||||
NetSpecificMode string `mapstructure:"net_specific_mode"`
|
||||
TitleBrand string `mapstructure:"title_brand"`
|
||||
NavBarBrand string `mapstructure:"navbar_brand"`
|
||||
NavBarBrandURL string `mapstructure:"navbar_brand_url"`
|
||||
NavBarAllServer string `mapstructure:"navbar_all_servers"`
|
||||
NavBarAllURL string `mapstructure:"navbar_all_url"`
|
||||
BgpmapInfo string `mapstructure:"bgpmap_info"`
|
||||
TelegramBotName string `mapstructure:"telegram_bot_name"`
|
||||
ProtocolFilter string `mapstructure:"protocol_filter"`
|
||||
NameFilter string `mapstructure:"name_filter"`
|
||||
TimeOut int `mapstructure:"timeout"`
|
||||
}
|
||||
|
||||
// Parse settings with viper, and convert to legacy setting format
|
||||
func parseSettings() {
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("/etc/bird-lg")
|
||||
viper.SetConfigName("bird-lg")
|
||||
viper.AllowEmptyEnv(true)
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvPrefix("birdlg")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
|
||||
|
||||
pflag.String("servers", "", "server name prefixes, separated by comma")
|
||||
viper.BindPFlag("servers", pflag.Lookup("servers"))
|
||||
|
||||
pflag.String("domain", "", "server name domain suffixes")
|
||||
viper.BindPFlag("domain", pflag.Lookup("domain"))
|
||||
|
||||
pflag.Int("proxy-port", 8000, "port bird-lgproxy is running on")
|
||||
viper.BindPFlag("proxy_port", pflag.Lookup("proxy-port"))
|
||||
|
||||
pflag.String("whois", "whois.verisign-grs.com", "whois server for queries")
|
||||
viper.BindPFlag("whois", pflag.Lookup("whois"))
|
||||
|
||||
pflag.String("listen", "5000", "address or unix socket bird-lg is listening on")
|
||||
viper.BindPFlag("listen", pflag.Lookup("listen"))
|
||||
|
||||
pflag.String("dns-interface", "asn.cymru.com", "dns zone to query ASN information")
|
||||
viper.BindPFlag("dns_interface", pflag.Lookup("dns-interface"))
|
||||
|
||||
pflag.String("net-specific-mode", "", "network specific operation mode, [(none)|dn42]")
|
||||
viper.BindPFlag("net_specific-mode", pflag.Lookup("net-specific-mode"))
|
||||
|
||||
pflag.String("title-brand", "Bird-lg Go", "prefix of page titles in browser tabs")
|
||||
viper.BindPFlag("title_brand", pflag.Lookup("title-brand"))
|
||||
|
||||
pflag.String("navbar-brand", "", "brand to show in the navigation bar")
|
||||
viper.BindPFlag("navbar_brand", pflag.Lookup("navbar-brand"))
|
||||
|
||||
pflag.String("navbar-brand-url", "/", "the url of the brand to show in the navigation bar")
|
||||
viper.BindPFlag("navbar_brand_url", pflag.Lookup("navbar-brand-url"))
|
||||
|
||||
pflag.String("navbar-all-servers", "All Servers", "the text of \"All servers\" button in the navigation bar")
|
||||
viper.BindPFlag("navbar_all_servers", pflag.Lookup("navbar-all-servers"))
|
||||
|
||||
pflag.String("navbar-all-url", "all", "the URL of \"All servers\" button")
|
||||
viper.BindPFlag("navbar_all_url", pflag.Lookup("navbar-all-url"))
|
||||
|
||||
pflag.String("bgpmap-info", "asn,as-name,ASName,descr", "the infos displayed in bgpmap, separated by comma, start with \":\" means allow multiline")
|
||||
viper.BindPFlag("bgpmap_info", pflag.Lookup("bgpmap-info"))
|
||||
|
||||
pflag.String("telegram-bot-name", "", "telegram bot name (used to filter @bot commands)")
|
||||
viper.BindPFlag("telegram_bot_name", pflag.Lookup("telegram-bot-name"))
|
||||
|
||||
pflag.String("protocol-filter", "",
|
||||
"protocol types to show in summary tables (comma separated list); defaults to all if not set")
|
||||
viper.BindPFlag("protocol_filter", pflag.Lookup("protocol-filter"))
|
||||
|
||||
pflag.String("name-filter", "", "protocol name regex to hide in summary tables (RE2 syntax); defaults to none if not set")
|
||||
viper.BindPFlag("name_filter", pflag.Lookup("name-filter"))
|
||||
|
||||
pflag.Int("time-out", 120, "time before request timed out, in seconds; defaults to 120 if not set")
|
||||
viper.BindPFlag("timeout", pflag.Lookup("time-out"))
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
println("Warning on reading config: " + err.Error())
|
||||
}
|
||||
|
||||
viperSettings := viperSettingType{}
|
||||
if err := viper.Unmarshal(&viperSettings); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
setting.servers = strings.Split(viperSettings.Servers, ",")
|
||||
setting.serversDisplay = strings.Split(viperSettings.Servers, ",")
|
||||
// Split server names of the form "DisplayName<Hostname>"
|
||||
for i, server := range setting.servers {
|
||||
pos := strings.Index(server, "<")
|
||||
if pos != -1 {
|
||||
setting.serversDisplay[i] = server[0:pos]
|
||||
setting.servers[i] = server[pos+1 : len(server)-1]
|
||||
}
|
||||
}
|
||||
|
||||
setting.domain = viperSettings.Domain
|
||||
setting.proxyPort = viperSettings.ProxyPort
|
||||
setting.whoisServer = viperSettings.WhoisServer
|
||||
setting.listen = viperSettings.Listen
|
||||
setting.dnsInterface = viperSettings.DNSInterface
|
||||
setting.netSpecificMode = viperSettings.NetSpecificMode
|
||||
setting.titleBrand = viperSettings.TitleBrand
|
||||
|
||||
setting.navBarBrand = viperSettings.NavBarBrand
|
||||
if setting.navBarBrand == "" {
|
||||
setting.navBarBrand = setting.titleBrand
|
||||
}
|
||||
|
||||
setting.navBarBrandURL = viperSettings.NavBarBrandURL
|
||||
setting.navBarAllServer = viperSettings.NavBarAllServer
|
||||
setting.navBarAllURL = viperSettings.NavBarAllURL
|
||||
setting.bgpmapInfo = viperSettings.BgpmapInfo
|
||||
setting.telegramBotName = viperSettings.TelegramBotName
|
||||
|
||||
if viperSettings.ProtocolFilter != "" {
|
||||
setting.protocolFilter = strings.Split(viperSettings.ProtocolFilter, ",")
|
||||
} else {
|
||||
setting.protocolFilter = []string{}
|
||||
}
|
||||
|
||||
setting.nameFilter = viperSettings.NameFilter
|
||||
setting.timeOut = viperSettings.TimeOut
|
||||
|
||||
fmt.Printf("%#v\n", setting)
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseSettings(t *testing.T) {
|
||||
parseSettings()
|
||||
// Good as long as it doesn't panic
|
||||
}
|
@ -31,8 +31,7 @@ type tgWebhookResponse struct {
|
||||
|
||||
func telegramIsCommand(message string, command string) bool {
|
||||
b := false
|
||||
b = b || strings.HasPrefix(message, "/"+command+"@"+setting.telegramBotName+" ")
|
||||
b = b || message == "/"+command+"@"+setting.telegramBotName
|
||||
b = b || strings.HasPrefix(message, "/"+command+"@")
|
||||
b = b || strings.HasPrefix(message, "/"+command+" ")
|
||||
b = b || message == "/"+command
|
||||
return b
|
||||
@ -105,7 +104,7 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
|
||||
} else if telegramIsCommand(request.Message.Text, "whois") {
|
||||
if setting.netSpecificMode == "dn42" || setting.netSpecificMode == "dn42_generic" {
|
||||
if setting.netSpecificMode == "dn42" {
|
||||
targetNumber, err := strconv.ParseUint(target, 10, 64)
|
||||
if err == nil {
|
||||
if targetNumber < 10000 {
|
||||
@ -119,8 +118,6 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
||||
tempResult := whois(target)
|
||||
if setting.netSpecificMode == "dn42" {
|
||||
commandResult = dn42WhoisFilter(tempResult)
|
||||
} else if setting.netSpecificMode == "dn42_shorten" || setting.netSpecificMode == "shorten" {
|
||||
commandResult = shortenWhoisFilter(tempResult)
|
||||
} else {
|
||||
commandResult = tempResult
|
||||
}
|
||||
@ -141,10 +138,6 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
|
||||
commandResult = "empty result"
|
||||
}
|
||||
|
||||
if len(commandResult) > 4096 {
|
||||
commandResult = commandResult[0:4096]
|
||||
}
|
||||
|
||||
// Create a JSON response
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
response := &tgWebhookResponse{
|
||||
|
@ -1,367 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func doTestTelegramIsCommand(t *testing.T, message string, command string, expected bool) {
|
||||
result := telegramIsCommand(message, command)
|
||||
assert.Equal(t, result, expected)
|
||||
}
|
||||
|
||||
func mockTelegramCall(t *testing.T, msg string, raw bool) string {
|
||||
return mockTelegramEndpointCall(t, "/telegram/", msg, raw)
|
||||
}
|
||||
|
||||
func mockTelegramEndpointCall(t *testing.T, endpoint string, msg string, raw bool) string {
|
||||
request := tgWebhookRequest{
|
||||
Message: tgMessage{
|
||||
MessageID: 123,
|
||||
Chat: tgChat{
|
||||
ID: 456,
|
||||
},
|
||||
Text: msg,
|
||||
},
|
||||
}
|
||||
requestJson, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
requestBody := bytes.NewReader(requestJson)
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, endpoint, requestBody)
|
||||
w := httptest.NewRecorder()
|
||||
webHandlerTelegramBot(w, r)
|
||||
|
||||
if raw {
|
||||
return w.Body.String()
|
||||
} else {
|
||||
var response tgWebhookResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, response.ChatID, request.Message.Chat.ID)
|
||||
assert.Equal(t, response.ReplyToMessageID, request.Message.MessageID)
|
||||
return response.Text
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramIsCommand(t *testing.T) {
|
||||
setting.telegramBotName = "test_bot"
|
||||
|
||||
// Recognize command
|
||||
doTestTelegramIsCommand(t, "/trace", "trace", true)
|
||||
doTestTelegramIsCommand(t, "/trace", "trace1234", false)
|
||||
doTestTelegramIsCommand(t, "/trace", "tra", false)
|
||||
doTestTelegramIsCommand(t, "/trace", "abcdefg", false)
|
||||
|
||||
// Recognize command with parameters
|
||||
doTestTelegramIsCommand(t, "/trace google.com", "trace", true)
|
||||
doTestTelegramIsCommand(t, "/trace google.com", "trace1234", false)
|
||||
doTestTelegramIsCommand(t, "/trace google.com", "tra", false)
|
||||
doTestTelegramIsCommand(t, "/trace google.com", "abcdefg", false)
|
||||
|
||||
// Recognize command with bot name
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot", "trace", true)
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot", "trace1234", false)
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot", "tra", false)
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot", "abcdefg", false)
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot_123", "trace", false)
|
||||
doTestTelegramIsCommand(t, "/trace@test_", "trace", false)
|
||||
|
||||
// Recognize command with bot name and parameters
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot google.com", "trace", true)
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot google.com", "trace1234", false)
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot google.com", "tra", false)
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot google.com", "abcdefg", false)
|
||||
doTestTelegramIsCommand(t, "/trace@test_bot_123 google.com", "trace", false)
|
||||
doTestTelegramIsCommand(t, "/trace@test google.com", "trace", false)
|
||||
}
|
||||
|
||||
func TestTelegramBatchRequestFormatSingleServer(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock")
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
result := telegramBatchRequestFormat(setting.servers, "mock", "cmd", telegramDefaultPostProcess)
|
||||
expected := "Mock\n\n"
|
||||
assert.Equal(t, result, expected)
|
||||
}
|
||||
|
||||
func TestTelegramBatchRequestFormatMultipleServers(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock")
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://beta:8000/mock?q=cmd", httpResponse)
|
||||
httpmock.RegisterResponder("GET", "http://gamma:8000/mock?q=cmd", httpResponse)
|
||||
|
||||
setting.servers = []string{
|
||||
"alpha",
|
||||
"beta",
|
||||
"gamma",
|
||||
}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
result := telegramBatchRequestFormat(setting.servers, "mock", "cmd", telegramDefaultPostProcess)
|
||||
expected := "alpha\nMock\n\nbeta\nMock\n\ngamma\nMock\n\n"
|
||||
assert.Equal(t, result, expected)
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotBadJSON(t *testing.T) {
|
||||
requestBody := strings.NewReader("{bad json}")
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/telegram/", requestBody)
|
||||
w := httptest.NewRecorder()
|
||||
webHandlerTelegramBot(w, r)
|
||||
|
||||
response := w.Body.String()
|
||||
assert.Equal(t, response, "")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotTrace(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock Response")
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/traceroute?q=1.1.1.1", httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
response := mockTelegramCall(t, "/trace 1.1.1.1", false)
|
||||
assert.Equal(t, response, "```\nMock Response\n```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotTraceWithServerList(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock Response")
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/traceroute?q=1.1.1.1", httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
response := mockTelegramEndpointCall(t, "/telegram/alpha", "/trace 1.1.1.1", false)
|
||||
assert.Equal(t, response, "```\nMock Response\n```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotRoute(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock Response")
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 primary"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
response := mockTelegramCall(t, "/route 1.1.1.1", false)
|
||||
assert.Equal(t, response, "```\nMock Response\n```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotPath(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, `
|
||||
BGP.as_path: 123 456
|
||||
`)
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all primary"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
response := mockTelegramCall(t, "/path 1.1.1.1", false)
|
||||
assert.Equal(t, response, "```\n123 456\n```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotPathMissing(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "No path in this response")
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all primary"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
response := mockTelegramCall(t, "/path 1.1.1.1", false)
|
||||
assert.Equal(t, response, "```\nempty result\n```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotWhois(t *testing.T) {
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS6939",
|
||||
response: AS6939Response,
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.netSpecificMode = ""
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
|
||||
response := mockTelegramCall(t, "/whois AS6939", false)
|
||||
assert.Equal(t, response, "```"+server.response+"```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotWhoisDN42Mode(t *testing.T) {
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS4242422547",
|
||||
response: `
|
||||
Query for AS4242422547
|
||||
`,
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.netSpecificMode = "dn42"
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
|
||||
response := mockTelegramCall(t, "/whois 2547", false)
|
||||
assert.Equal(t, response, "```"+server.response+"```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotWhoisDN42ModeFullASN(t *testing.T) {
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS4242422547",
|
||||
response: `
|
||||
Query for AS4242422547
|
||||
`,
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.netSpecificMode = "dn42"
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
|
||||
response := mockTelegramCall(t, "/whois 4242422547", false)
|
||||
assert.Equal(t, response, "```"+server.response+"```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotWhoisShortenMode(t *testing.T) {
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS6939",
|
||||
response: `
|
||||
Information line that will be removed
|
||||
|
||||
# Comment that will be removed
|
||||
Name: Redacted for privacy
|
||||
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
|
||||
Looooooooooooooooooooooong key: this line will be skipped.
|
||||
|
||||
Preserved1: this line isn't removed.
|
||||
Preserved2: this line isn't removed.
|
||||
Preserved3: this line isn't removed.
|
||||
Preserved4: this line isn't removed.
|
||||
Preserved5: this line isn't removed.
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
expectedResult := `Preserved1: this line isn't removed.
|
||||
Preserved2: this line isn't removed.
|
||||
Preserved3: this line isn't removed.
|
||||
Preserved4: this line isn't removed.
|
||||
Preserved5: this line isn't removed.
|
||||
|
||||
3 line(s) skipped.`
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.netSpecificMode = "shorten"
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
|
||||
response := mockTelegramCall(t, "/whois AS6939", false)
|
||||
assert.Equal(t, response, "```\n"+expectedResult+"\n```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotHelp(t *testing.T) {
|
||||
response := mockTelegramCall(t, "/help", false)
|
||||
if !strings.Contains(response, "/trace") {
|
||||
t.Error("Did not get help message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotUnknownCommand(t *testing.T) {
|
||||
response := mockTelegramCall(t, "/nonexistent", true)
|
||||
assert.Equal(t, response, "")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotNotCommand(t *testing.T) {
|
||||
response := mockTelegramCall(t, "random chat message", true)
|
||||
assert.Equal(t, response, "")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotEmptyResponse(t *testing.T) {
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS6939",
|
||||
response: "",
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.netSpecificMode = ""
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
|
||||
response := mockTelegramCall(t, "/whois AS6939", false)
|
||||
assert.Equal(t, response, "```\nempty result\n```")
|
||||
}
|
||||
|
||||
func TestWebHandlerTelegramBotTruncateLongResponse(t *testing.T) {
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS6939",
|
||||
response: strings.Repeat("A", 65536),
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.netSpecificMode = ""
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
|
||||
response := mockTelegramCall(t, "/whois AS6939", false)
|
||||
assert.Equal(t, response, "```\n"+strings.Repeat("A", 4096)+"\n```")
|
||||
}
|
@ -1,32 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// import templates and other assets
|
||||
//go:embed assets
|
||||
var assets embed.FS
|
||||
|
||||
const TEMPLATE_PATH = "assets/templates/"
|
||||
|
||||
// template argument structures
|
||||
|
||||
// page
|
||||
type TemplatePage struct {
|
||||
// Global options
|
||||
Options map[string]string
|
||||
Servers []string
|
||||
ServersDisplay []string
|
||||
Options map[string]string
|
||||
Servers []string
|
||||
|
||||
// Parameters related to current request
|
||||
AllServersLinkActive bool
|
||||
AllServerTitle string
|
||||
AllServersURL string
|
||||
AllServersURLCustom string
|
||||
|
||||
// Whois specific handling (for its unique URL)
|
||||
IsWhois bool
|
||||
@ -37,21 +26,21 @@ type TemplatePage struct {
|
||||
URLCommand string
|
||||
|
||||
// Generated content to be displayed
|
||||
Title string
|
||||
Brand string
|
||||
BrandURL string
|
||||
Content template.HTML
|
||||
Title string
|
||||
Brand string
|
||||
Content string
|
||||
}
|
||||
|
||||
// summary
|
||||
|
||||
type SummaryRowData struct {
|
||||
Name string `json:"name"`
|
||||
Proto string `json:"proto"`
|
||||
Table string `json:"table"`
|
||||
State string `json:"state"`
|
||||
MappedState string `json:"-"`
|
||||
Since string `json:"since"`
|
||||
Info string `json:"info"`
|
||||
Name string
|
||||
Proto string
|
||||
Table string
|
||||
State string
|
||||
MappedState string
|
||||
Since string
|
||||
Info string
|
||||
}
|
||||
|
||||
// utility functions to allow filtering of results in the template
|
||||
@ -74,7 +63,7 @@ type TemplateSummary struct {
|
||||
// whois
|
||||
type TemplateWhois struct {
|
||||
Target string
|
||||
Result template.HTML
|
||||
Result string
|
||||
}
|
||||
|
||||
// bgpmap
|
||||
@ -88,7 +77,7 @@ type TemplateBGPmap struct {
|
||||
type TemplateBird struct {
|
||||
ServerName string
|
||||
Target string
|
||||
Result template.HTML
|
||||
Result string
|
||||
}
|
||||
|
||||
// global variable to hold the templates
|
||||
@ -105,13 +94,7 @@ var requiredTemplates = [...]string{
|
||||
"bird",
|
||||
}
|
||||
|
||||
// define functions to be made available in templates
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"pathescape": url.PathEscape,
|
||||
}
|
||||
|
||||
// import templates from embedded assets
|
||||
// import templates from bindata
|
||||
|
||||
func ImportTemplates() {
|
||||
|
||||
@ -121,16 +104,13 @@ func ImportTemplates() {
|
||||
// for each template that is needed
|
||||
for _, tmpl := range requiredTemplates {
|
||||
|
||||
// extract the template definition from the embedded assets
|
||||
def, err := assets.ReadFile(TEMPLATE_PATH + tmpl + ".tpl")
|
||||
if err != nil {
|
||||
panic("Unable to read template (" + TEMPLATE_PATH + tmpl + ": " + err.Error())
|
||||
}
|
||||
// extract the template definition from the bindata
|
||||
def := MustAssetString("templates/" + tmpl + ".tpl")
|
||||
|
||||
// and add it to the template library
|
||||
template, err := template.New(tmpl).Funcs(funcMap).Parse(string(def))
|
||||
template, err := template.New(tmpl).Parse(def)
|
||||
if err != nil {
|
||||
panic("Unable to parse template (" + TEMPLATE_PATH + tmpl + ": " + err.Error())
|
||||
panic("Unable to parse template (templates/" + tmpl + ": " + err.Error())
|
||||
}
|
||||
|
||||
// store in the library
|
||||
|
@ -1,25 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func TestSummaryRowDataNameHasPrefix(t *testing.T) {
|
||||
data := SummaryRowData{
|
||||
Name: "mock",
|
||||
}
|
||||
|
||||
assert.Equal(t, data.NameHasPrefix("m"), true)
|
||||
assert.Equal(t, data.NameHasPrefix("n"), false)
|
||||
}
|
||||
|
||||
func TestSummaryRowDataNameContains(t *testing.T) {
|
||||
data := SummaryRowData{
|
||||
Name: "mock",
|
||||
}
|
||||
|
||||
assert.Equal(t, data.NameContains("oc"), true)
|
||||
assert.Equal(t, data.NameContains("no"), false)
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
Table master4:
|
||||
172.20.0.53/32 unicast [ibgp_sjc2 2023-04-29 from fd86:bad:11b7:22::1] * (100/38) [AS4242423914i]
|
||||
via 169.254.108.122 on igp-sjc2
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242423914
|
||||
BGP.next_hop: 172.20.229.122
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,1) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242421080, 101, 44) (4242421080, 103, 122) (4242421080, 104, 1)
|
||||
unicast [miaotony_2688 2023-04-29 from fe80::2688] (100) [AS4242423914i]
|
||||
via 172.23.6.6 on dn42las-miaoton
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242422688 4242423914
|
||||
BGP.next_hop: 172.23.6.6
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,3) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [imlonghao_1888 2023-04-17] (100) [AS4242423914i]
|
||||
via fe80::1888 on dn42-imlonghao
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242421888 4242423914
|
||||
BGP.next_hop: :: fe80::1888
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,1) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [ciplc_3021 2023-04-29 from fe80::943e] (100) [AS4242423914i]
|
||||
via 172.23.33.161 on dn42-ciplc
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242423021 4242423914
|
||||
BGP.next_hop: 172.23.33.161
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,1) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [iedon_2189 2023-04-29 from fe80::2189:ef] (100) [AS4242423914i]
|
||||
via 172.23.91.114 on dn42-iedon
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242422189 4242423914
|
||||
BGP.next_hop: 172.23.91.114
|
||||
BGP.med: 65
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,24) (64511,33) (64511,3)
|
||||
BGP.large_community: (4242422189, 1, 4) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [prevarinite_2475 2023-04-19] (100) [AS4242423914i]
|
||||
via fe80::7072:6576:6172:1 on dn42-prevarinit
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242422475 4242423192 4242423914
|
||||
BGP.next_hop: :: fe80::7072:6576:6172:1
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,1) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [lare_3035 2023-04-29] (100) [AS4242423914i]
|
||||
via fe80::3035:132 on dn42-lare
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242423035 4242423914
|
||||
BGP.next_hop: :: fe80::3035:132
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,3) (64511,34) (64511,24)
|
||||
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [hinata_3724 2023-04-29 from fe80::3724] (100) [AS4242423914i]
|
||||
via 172.23.215.228 on dn42las-hinata
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242423724 4201271111 4242423914
|
||||
BGP.next_hop: 172.23.215.228
|
||||
BGP.med: 70
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,22) (64511,1) (64511,34)
|
||||
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [liki4_0927 2023-04-21] (100) [AS4242423914i]
|
||||
via fe80::927 on dn42-liki4
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242420927 4242421888 4242423914
|
||||
BGP.next_hop: :: fe80::927
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,2) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [eastbound_2633 2023-04-29 from fe80::2633] (100) [AS4242423914i]
|
||||
via 172.23.250.42 on dn42las-eastbnd
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242422633 4242423914
|
||||
BGP.next_hop: 172.23.250.42
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,24) (64511,34) (64511,3)
|
||||
BGP.large_community: (4242422633, 101, 44) (4242422633, 103, 36) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [yura_2464 2023-04-29] (100) [AS4242423914i]
|
||||
via fe80::2464 on dn42las-yura
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242422464 4242423914
|
||||
BGP.next_hop: :: fe80::2464
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,1) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242422464, 2, 4242423914) (4242422464, 64511, 44) (4242422464, 64511, 1840) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
||||
unicast [ibgp_fra 2023-04-29 from fd86:bad:11b7:117::1] (100/186) [AS4242423914i]
|
||||
via 169.254.108.113 on igp-chi
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242423914
|
||||
BGP.next_hop: 172.20.229.117
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,1) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242421080, 101, 41) (4242421080, 103, 117) (4242421080, 104, 3)
|
||||
unicast [ibgp_sgp 2023-04-29 from fd86:bad:11b7:239::1] (100/200) [AS4242423914i]
|
||||
via 169.254.108.39 on igp-sgp
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242423914
|
||||
BGP.next_hop: 172.22.108.39
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,4) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242421080, 101, 51) (4242421080, 103, 39) (4242421080, 104, 4)
|
||||
unicast [ibgp_ymq 2023-04-30 from fd86:bad:11b7:23::1] (100/105) [AS4242423914i]
|
||||
via 169.254.108.113 on igp-chi
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242423914
|
||||
BGP.next_hop: 172.20.229.123
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,3) (64511,24) (64511,34)
|
||||
BGP.large_community: (4242421080, 101, 42) (4242421080, 103, 123) (4242421080, 104, 2)
|
||||
unicast [cola_3391 18:41:16.608 from fe80::3391] (100) [AS4242423914i]
|
||||
via 172.22.96.65 on dn42-cola
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242423391 4242420604 4242423914
|
||||
BGP.next_hop: 172.22.96.65
|
||||
BGP.med: 50
|
||||
BGP.local_pref: 100
|
||||
BGP.community: (64511,4) (64511,34) (64511,24)
|
||||
BGP.large_community: (4242420604, 2, 50) (4242420604, 501, 4242423914) (4242420604, 502, 44) (4242420604, 504, 4) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
|
@ -2,41 +2,27 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
var primitiveMap = map[string]string{
|
||||
"summary": "show protocols",
|
||||
"detail": "show protocols all %s",
|
||||
"route_from_protocol": "show route protocol %s",
|
||||
"route_from_protocol_all": "show route protocol %s all",
|
||||
"route_from_protocol_primary": "show route protocol %s primary",
|
||||
"route_from_protocol_all_primary": "show route protocol %s all primary",
|
||||
"route_filtered_from_protocol": "show route filtered protocol %s",
|
||||
"route_filtered_from_protocol_all": "show route filtered protocol %s all",
|
||||
"route_from_origin": "show route where bgp_path.last = %s",
|
||||
"route_from_origin_all": "show route where bgp_path.last = %s all",
|
||||
"route_from_origin_primary": "show route where bgp_path.last = %s primary",
|
||||
"route_from_origin_all_primary": "show route where bgp_path.last = %s all primary",
|
||||
"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",
|
||||
"whois": "%s",
|
||||
"traceroute": "%s",
|
||||
"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",
|
||||
}
|
||||
|
||||
// serve up a generic error
|
||||
@ -69,7 +55,7 @@ func webHandlerWhois(w http.ResponseWriter, r *http.Request) {
|
||||
renderPageTemplate(
|
||||
w, r,
|
||||
" - whois "+html.EscapeString(target),
|
||||
template.HTML(buffer.String()),
|
||||
buffer.String(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -85,13 +71,12 @@ func webBackendCommunicator(endpoint string, command string) func(w http.Respons
|
||||
split := strings.SplitN(r.URL.Path[1:], "/", 3)
|
||||
var urlCommands string
|
||||
if len(split) >= 3 {
|
||||
urlCommands = split[2]
|
||||
}
|
||||
|
||||
if (command == "generic") && !(urlCommands == "memory" || urlCommands == "status") {
|
||||
renderPageTemplate(w, r, " - "+html.EscapeString(endpoint+" show "+urlCommands),
|
||||
template.HTML("<h2>'show "+urlCommands+"' not supported</h2>"))
|
||||
return
|
||||
tmp, err := url.PathUnescape(split[2])
|
||||
if err != nil {
|
||||
serverError(w, r)
|
||||
return
|
||||
}
|
||||
urlCommands = tmp
|
||||
}
|
||||
|
||||
var backendCommand string
|
||||
@ -102,30 +87,27 @@ func webBackendCommunicator(endpoint string, command string) func(w http.Respons
|
||||
}
|
||||
backendCommand = strings.TrimSpace(backendCommand)
|
||||
|
||||
servers := strings.Split(split[1], "+")
|
||||
escapedServers, err := url.PathUnescape(split[1])
|
||||
if err != nil {
|
||||
serverError(w, r)
|
||||
return
|
||||
}
|
||||
servers := strings.Split(escapedServers, "+")
|
||||
|
||||
var responses []string = batchRequest(servers, endpoint, backendCommand)
|
||||
var content string
|
||||
for i, response := range responses {
|
||||
|
||||
var result template.HTML
|
||||
var result string
|
||||
if (endpoint == "bird") && backendCommand == "show protocols" && len(response) > 4 && strings.ToLower(response[0:4]) == "name" {
|
||||
result = summaryTable(response, servers[i])
|
||||
} else {
|
||||
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,
|
||||
ServerName: servers[i],
|
||||
Target: backendCommand,
|
||||
Result: result,
|
||||
}
|
||||
@ -142,8 +124,8 @@ func webBackendCommunicator(endpoint string, command string) func(w http.Respons
|
||||
|
||||
renderPageTemplate(
|
||||
w, r,
|
||||
" - "+endpoint+" "+backendCommand,
|
||||
template.HTML(content),
|
||||
" - "+html.EscapeString(endpoint+" "+backendCommand),
|
||||
content,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -173,15 +155,11 @@ func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWrite
|
||||
var servers []string = strings.Split(split[1], "+")
|
||||
var responses []string = batchRequest(servers, endpoint, backendCommand)
|
||||
|
||||
// encode result with base64 to prevent xss
|
||||
result := birdRouteToGraphviz(servers, responses, urlCommands)
|
||||
result = base64.StdEncoding.EncodeToString([]byte(result))
|
||||
|
||||
// render the bgpmap result template
|
||||
args := TemplateBGPmap{
|
||||
Servers: servers,
|
||||
Target: backendCommand,
|
||||
Result: result,
|
||||
Result: birdRouteToGraphviz(servers, responses, urlCommands),
|
||||
}
|
||||
|
||||
tmpl := TemplateLibrary["bgpmap"]
|
||||
@ -194,46 +172,54 @@ func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWrite
|
||||
renderPageTemplate(
|
||||
w, r,
|
||||
" - "+html.EscapeString(endpoint+" "+backendCommand),
|
||||
template.HTML(buffer.String()),
|
||||
buffer.String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// redirect from the form input to a path style query
|
||||
func webHandlerNavbarFormRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
action := query.Get("action")
|
||||
|
||||
switch action {
|
||||
case "whois":
|
||||
target := url.PathEscape(query.Get("target"))
|
||||
http.Redirect(w, r, "/"+action+"/"+target, 302)
|
||||
case "summary":
|
||||
server := url.PathEscape(query.Get("server"))
|
||||
http.Redirect(w, r, "/"+action+"/"+server+"/", 302)
|
||||
default:
|
||||
server := url.PathEscape(query.Get("server"))
|
||||
target := url.PathEscape(query.Get("target"))
|
||||
http.Redirect(w, r, "/"+action+"/"+server+"/"+target, 302)
|
||||
}
|
||||
}
|
||||
|
||||
// set up routing paths and start webserver
|
||||
func webServerStart(l net.Listener) {
|
||||
func webServerStart() {
|
||||
|
||||
// redirect main page to all server summary
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/summary/"+url.PathEscape(strings.Join(setting.servers, "+")), 302)
|
||||
http.Redirect(w, r, "/summary/"+strings.Join(setting.servers, "+"), 302)
|
||||
})
|
||||
|
||||
// serve static pages using embedded assets from template.go
|
||||
subfs, err := fs.Sub(assets, "assets")
|
||||
if err != nil {
|
||||
panic("Webserver fs.sub failed: " + err.Error())
|
||||
}
|
||||
fs := http.FileServer(http.FS(subfs))
|
||||
|
||||
http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||
fs.ServeHTTP(w, r)
|
||||
// serve static pages using the AssetFS and bindata
|
||||
fs := http.FileServer(&assetfs.AssetFS{
|
||||
Asset: Asset,
|
||||
AssetDir: AssetDir,
|
||||
AssetInfo: AssetInfo,
|
||||
Prefix: "",
|
||||
})
|
||||
|
||||
http.Handle("/static/", fs)
|
||||
http.Handle("/robots.txt", fs)
|
||||
http.Handle("/favicon.ico", fs)
|
||||
|
||||
// backend routes
|
||||
http.HandleFunc("/summary/", webBackendCommunicator("bird", "summary"))
|
||||
http.HandleFunc("/detail/", webBackendCommunicator("bird", "detail"))
|
||||
http.HandleFunc("/route_filtered_from_protocol/", webBackendCommunicator("bird", "route_filtered_from_protocol"))
|
||||
http.HandleFunc("/route_filtered_from_protocol_all/", webBackendCommunicator("bird", "route_filtered_from_protocol_all"))
|
||||
http.HandleFunc("/route_from_protocol/", webBackendCommunicator("bird", "route_from_protocol"))
|
||||
http.HandleFunc("/route_from_protocol_all/", webBackendCommunicator("bird", "route_from_protocol_all"))
|
||||
http.HandleFunc("/route_from_protocol_primary/", webBackendCommunicator("bird", "route_from_protocol_primary"))
|
||||
http.HandleFunc("/route_from_protocol_all_primary/", webBackendCommunicator("bird", "route_from_protocol_all_primary"))
|
||||
http.HandleFunc("/route_from_origin/", webBackendCommunicator("bird", "route_from_origin"))
|
||||
http.HandleFunc("/route_from_origin_all/", webBackendCommunicator("bird", "route_from_origin_all"))
|
||||
http.HandleFunc("/route_from_origin_primary/", webBackendCommunicator("bird", "route_from_origin_primary"))
|
||||
http.HandleFunc("/route_from_origin_all_primary/", webBackendCommunicator("bird", "route_from_origin_all_primary"))
|
||||
http.HandleFunc("/route/", webBackendCommunicator("bird", "route"))
|
||||
http.HandleFunc("/route_all/", webBackendCommunicator("bird", "route_all"))
|
||||
http.HandleFunc("/route_bgpmap/", webHandlerBGPMap("bird", "route_bgpmap"))
|
||||
@ -244,9 +230,9 @@ func webServerStart(l net.Listener) {
|
||||
http.HandleFunc("/generic/", webBackendCommunicator("bird", "generic"))
|
||||
http.HandleFunc("/traceroute/", webBackendCommunicator("traceroute", "traceroute"))
|
||||
http.HandleFunc("/whois/", webHandlerWhois)
|
||||
// http.HandleFunc("/api/", apiHandler)
|
||||
// http.HandleFunc("/telegram/", webHandlerTelegramBot)
|
||||
http.HandleFunc("/redir", webHandlerNavbarFormRedirect)
|
||||
http.HandleFunc("/telegram/", webHandlerTelegramBot)
|
||||
|
||||
// Start HTTP server
|
||||
http.Serve(l, handlers.LoggingHandler(os.Stdout, http.DefaultServeMux))
|
||||
http.ListenAndServe(setting.listen, handlers.LoggingHandler(os.Stdout, http.DefaultServeMux))
|
||||
}
|
||||
|
@ -1,89 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func TestServerError(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/error", nil)
|
||||
w := httptest.NewRecorder()
|
||||
serverError(w, r)
|
||||
assert.Equal(t, w.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func TestWebHandlerWhois(t *testing.T) {
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS6939",
|
||||
response: AS6939Response,
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.netSpecificMode = ""
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/whois/AS6939", nil)
|
||||
w := httptest.NewRecorder()
|
||||
webHandlerWhois(w, r)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
if !strings.Contains(w.Body.String(), "HURRICANE") {
|
||||
t.Error("Body does not contain whois result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebBackendCommunicator(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
|
||||
httpResponse := httpmock.NewStringResponder(200, input)
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
setting.dnsInterface = ""
|
||||
setting.whoisServer = ""
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/route_bgpmap/alpha/1.1.1.1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := webBackendCommunicator("bird", "route_all")
|
||||
handler(w, r)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestWebHandlerBGPMap(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
|
||||
httpResponse := httpmock.NewStringResponder(200, input)
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
setting.dnsInterface = ""
|
||||
setting.whoisServer = ""
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/route_bgpmap/alpha/1.1.1.1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := webHandlerBGPMap("bird", "route_bgpmap")
|
||||
handler(w, r)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
}
|
@ -1,59 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/shlex"
|
||||
)
|
||||
|
||||
// Send a whois request
|
||||
func whois(s string) string {
|
||||
if setting.whoisServer == "" {
|
||||
return ""
|
||||
conn, err := net.Dial("tcp", setting.whoisServer+":43")
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if strings.HasPrefix(setting.whoisServer, "/") {
|
||||
args, err := shlex.Split(setting.whoisServer)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
args = append(args, s)
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if len(output) > 65535 {
|
||||
output = output[:65535]
|
||||
}
|
||||
if err != nil {
|
||||
return err.Error() + "\n" + string(output)
|
||||
} else {
|
||||
return string(output)
|
||||
}
|
||||
} else {
|
||||
buf := make([]byte, 65536)
|
||||
|
||||
whoisServer := setting.whoisServer
|
||||
if !strings.Contains(whoisServer, ":") {
|
||||
whoisServer = whoisServer + ":43"
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", whoisServer, 5*time.Second)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.Write([]byte(s + "\r\n"))
|
||||
|
||||
n, err := io.ReadFull(conn, buf)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
return err.Error() + "\n" + string(buf[:n])
|
||||
}
|
||||
return string(buf[:n])
|
||||
conn.Write([]byte(s + "\r\n"))
|
||||
result, err := ioutil.ReadAll(conn)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
@ -1,128 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type WhoisServer struct {
|
||||
t *testing.T
|
||||
expectedQuery string
|
||||
response string
|
||||
server net.Listener
|
||||
}
|
||||
|
||||
const AS6939Response = `
|
||||
ASNumber: 6939
|
||||
ASName: HURRICANE
|
||||
ASHandle: AS6939
|
||||
RegDate: 1996-06-28
|
||||
Updated: 2003-11-04
|
||||
Ref: https://rdap.arin.net/registry/autnum/6939
|
||||
`
|
||||
|
||||
func (s *WhoisServer) Listen() {
|
||||
var err error
|
||||
s.server, err = net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
s.t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WhoisServer) Run() {
|
||||
for {
|
||||
conn, err := s.server.Accept()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if conn == nil {
|
||||
break
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
query, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if strings.TrimSpace(string(query)) != s.expectedQuery {
|
||||
s.t.Errorf("Query %s doesn't match expectation %s", string(query), s.expectedQuery)
|
||||
}
|
||||
conn.Write([]byte(s.response))
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WhoisServer) Close() {
|
||||
if s.server == nil {
|
||||
return
|
||||
}
|
||||
s.server.Close()
|
||||
}
|
||||
|
||||
func TestWhois(t *testing.T) {
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS6939",
|
||||
response: AS6939Response,
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
result := whois("AS6939")
|
||||
if !strings.Contains(result, "HURRICANE") {
|
||||
t.Errorf("Whois AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoisWithoutServer(t *testing.T) {
|
||||
setting.whoisServer = ""
|
||||
result := whois("AS6939")
|
||||
if result != "" {
|
||||
t.Errorf("Whois AS6939 without server produced output, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoisConnectionError(t *testing.T) {
|
||||
setting.whoisServer = "127.0.0.1:0"
|
||||
result := whois("AS6939")
|
||||
if !strings.Contains(result, "connect: connection refused") {
|
||||
t.Errorf("Whois AS6939 without server produced output, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoisHostProcess(t *testing.T) {
|
||||
setting.whoisServer = "/bin/sh -c \"echo Mock Result\""
|
||||
result := whois("AS6939")
|
||||
if result != "Mock Result\n" {
|
||||
t.Errorf("Whois didn't produce expected result, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoisHostProcessMalformedCommand(t *testing.T) {
|
||||
setting.whoisServer = "/bin/sh -c \"mock"
|
||||
result := whois("AS6939")
|
||||
if result != "EOF found when expecting closing quote" {
|
||||
t.Errorf("Whois didn't produce expected result, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoisHostProcessError(t *testing.T) {
|
||||
setting.whoisServer = "/nonexistent"
|
||||
result := whois("AS6939")
|
||||
if !strings.Contains(result, "no such file or directory") {
|
||||
t.Errorf("Whois didn't produce expected result, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoisHostProcessVeryLong(t *testing.T) {
|
||||
setting.whoisServer = "/bin/sh -c \"for i in $(seq 1 131072); do printf 'A'; done\""
|
||||
result := whois("AS6939")
|
||||
if len(result) != 65535 {
|
||||
t.Errorf("Whois result incorrectly truncated, actual len %d", len(result))
|
||||
}
|
||||
}
|
@ -1,28 +1,28 @@
|
||||
FROM golang AS step_0
|
||||
|
||||
ENV CGO_ENABLED=0 GO111MODULE=on
|
||||
FROM golang:buster AS step_0
|
||||
#
|
||||
# IMAGE_ARCH is the binary format of the final output
|
||||
#
|
||||
ARG IMAGE_ARCH=amd64
|
||||
#
|
||||
ENV CGO_ENABLED=0 GOOS=linux GOARCH=$IMAGE_ARCH GO111MODULE=on
|
||||
WORKDIR /root
|
||||
COPY . .
|
||||
RUN go build -ldflags "-w -s" -o /proxy
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM alpine:edge AS step_1
|
||||
|
||||
FROM amd64/debian AS step_1
|
||||
ENV TARGET_ARCH=x86_64
|
||||
WORKDIR /root
|
||||
RUN apk add --no-cache build-base linux-headers
|
||||
|
||||
RUN wget https://sourceforge.net/projects/traceroute/files/traceroute/traceroute-2.1.3/traceroute-2.1.3.tar.gz/download \
|
||||
-O traceroute-2.1.3.tar.gz
|
||||
RUN tar xvf traceroute-2.1.3.tar.gz \
|
||||
&& cd traceroute-2.1.3 \
|
||||
&& make -j4 LDFLAGS="-static" \
|
||||
&& strip /root/traceroute-2.1.3/traceroute/traceroute
|
||||
|
||||
################################################################################
|
||||
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.3/traceroute/traceroute /
|
||||
COPY --from=step_1 /root/traceroute-2.1.0/traceroute/traceroute /
|
||||
ENTRYPOINT ["/proxy"]
|
||||
|
@ -1,3 +0,0 @@
|
||||
.PHONY: all
|
||||
all: $(shell find . -name \*.go -type f)
|
||||
go build -ldflags "-w -s" -o proxy
|
@ -1,30 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const MAX_LINE_SIZE = 1024
|
||||
|
||||
// Read a line from bird socket, removing preceding status number, output it.
|
||||
// Returns if there are more lines.
|
||||
func birdReadln(bird io.Reader, w io.Writer) bool {
|
||||
// Read from socket byte by byte, until reaching newline character
|
||||
c := make([]byte, MAX_LINE_SIZE)
|
||||
c := make([]byte, 1024, 1024)
|
||||
pos := 0
|
||||
for {
|
||||
// Leave one byte for newline character
|
||||
if pos >= MAX_LINE_SIZE-1 {
|
||||
if pos >= 1024 {
|
||||
break
|
||||
}
|
||||
_, err := bird.Read(c[pos : pos+1])
|
||||
if err != nil {
|
||||
w.Write([]byte(err.Error()))
|
||||
return false
|
||||
panic(err)
|
||||
}
|
||||
if c[pos] == byte('\n') {
|
||||
break
|
||||
@ -33,7 +27,6 @@ func birdReadln(bird io.Reader, w io.Writer) bool {
|
||||
}
|
||||
|
||||
c = c[:pos+1]
|
||||
c[pos] = '\n'
|
||||
// print(string(c[:]))
|
||||
|
||||
// Remove preceding status number, different situations
|
||||
@ -66,21 +59,13 @@ func birdHandler(httpW http.ResponseWriter, httpR *http.Request) {
|
||||
// Initialize BIRDv4 socket
|
||||
bird, err := net.Dial("unix", setting.birdSocket)
|
||||
if err != nil {
|
||||
httpW.WriteHeader(http.StatusInternalServerError)
|
||||
httpW.Write([]byte(err.Error()))
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
defer bird.Close()
|
||||
|
||||
birdReadln(bird, nil)
|
||||
birdWriteln(bird, "restrict")
|
||||
var restrictedConfirmation bytes.Buffer
|
||||
birdReadln(bird, &restrictedConfirmation)
|
||||
if !strings.Contains(restrictedConfirmation.String(), "Access restricted") {
|
||||
httpW.WriteHeader(http.StatusInternalServerError)
|
||||
httpW.Write([]byte("could not verify that bird access was restricted"))
|
||||
return
|
||||
}
|
||||
birdReadln(bird, nil)
|
||||
birdWriteln(bird, query)
|
||||
for birdReadln(bird, httpW) {
|
||||
}
|
||||
|
@ -1,213 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
type BirdServer struct {
|
||||
t *testing.T
|
||||
expectedQuery string
|
||||
response string
|
||||
server net.Listener
|
||||
socket string
|
||||
injectError string
|
||||
}
|
||||
|
||||
func (s *BirdServer) initSocket() {
|
||||
tmpDir, err := ioutil.TempDir("", "bird-lgproxy-go-mock")
|
||||
if err != nil {
|
||||
s.t.Fatal(err)
|
||||
}
|
||||
s.socket = path.Join(tmpDir, "mock.socket")
|
||||
}
|
||||
|
||||
func (s *BirdServer) Listen() {
|
||||
s.initSocket()
|
||||
|
||||
var err error
|
||||
s.server, err = net.Listen("unix", s.socket)
|
||||
if err != nil {
|
||||
s.t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BirdServer) Run() {
|
||||
for {
|
||||
conn, err := s.server.Accept()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if conn == nil {
|
||||
break
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
conn.Write([]byte("1234 Hello from mock bird\n"))
|
||||
|
||||
query, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if strings.TrimSpace(string(query)) != "restrict" {
|
||||
s.t.Errorf("Did not restrict bird permissions")
|
||||
}
|
||||
if s.injectError == "restriction" {
|
||||
conn.Write([]byte("1234 Restriction is disabled!\n"))
|
||||
} else {
|
||||
conn.Write([]byte("1234 Access restricted\n"))
|
||||
}
|
||||
|
||||
query, err = reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if strings.TrimSpace(string(query)) != s.expectedQuery {
|
||||
s.t.Errorf("Query %s doesn't match expectation %s", string(query), s.expectedQuery)
|
||||
}
|
||||
|
||||
responseList := strings.Split(s.response, "\n")
|
||||
for i := range responseList {
|
||||
if i == len(responseList)-1 {
|
||||
if s.injectError == "eof" {
|
||||
conn.Write([]byte("0000 " + responseList[i]))
|
||||
} else {
|
||||
conn.Write([]byte("0000 " + responseList[i] + "\n"))
|
||||
}
|
||||
} else {
|
||||
conn.Write([]byte("1234 " + responseList[i] + "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BirdServer) Close() {
|
||||
if s.server == nil {
|
||||
return
|
||||
}
|
||||
s.server.Close()
|
||||
}
|
||||
|
||||
func TestBirdReadln(t *testing.T) {
|
||||
input := strings.NewReader("1234 Bird Message\n")
|
||||
var output bytes.Buffer
|
||||
birdReadln(input, &output)
|
||||
|
||||
assert.Equal(t, output.String(), "Bird Message\n")
|
||||
}
|
||||
|
||||
func TestBirdReadlnNoPrefix(t *testing.T) {
|
||||
input := strings.NewReader(" Message without prefix\n")
|
||||
var output bytes.Buffer
|
||||
birdReadln(input, &output)
|
||||
|
||||
assert.Equal(t, output.String(), "Message without prefix\n")
|
||||
}
|
||||
|
||||
func TestBirdReadlnVeryLongLine(t *testing.T) {
|
||||
input := strings.NewReader(strings.Repeat("A", 4096))
|
||||
var output bytes.Buffer
|
||||
birdReadln(input, &output)
|
||||
|
||||
assert.Equal(t, output.String(), strings.Repeat("A", 1022)+"\n")
|
||||
}
|
||||
|
||||
func TestBirdWriteln(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
birdWriteln(&output, "Test command")
|
||||
assert.Equal(t, output.String(), "Test command\n")
|
||||
}
|
||||
|
||||
func TestBirdHandlerWithoutQuery(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/bird", nil)
|
||||
w := httptest.NewRecorder()
|
||||
birdHandler(w, r)
|
||||
}
|
||||
|
||||
func TestBirdHandlerWithQuery(t *testing.T) {
|
||||
server := BirdServer{
|
||||
t: t,
|
||||
expectedQuery: "show protocols",
|
||||
response: "Mock Response\nSecond Line",
|
||||
injectError: "",
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.birdSocket = server.socket
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape(server.expectedQuery), nil)
|
||||
w := httptest.NewRecorder()
|
||||
birdHandler(w, r)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), server.response+"\n")
|
||||
}
|
||||
|
||||
func TestBirdHandlerWithBadSocket(t *testing.T) {
|
||||
setting.birdSocket = "/nonexistent.sock"
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape("mock"), nil)
|
||||
w := httptest.NewRecorder()
|
||||
birdHandler(w, r)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func TestBirdHandlerWithoutRestriction(t *testing.T) {
|
||||
server := BirdServer{
|
||||
t: t,
|
||||
expectedQuery: "show protocols",
|
||||
response: "Mock Response",
|
||||
injectError: "restriction",
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.birdSocket = server.socket
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape("mock"), nil)
|
||||
w := httptest.NewRecorder()
|
||||
birdHandler(w, r)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func TestBirdHandlerEOF(t *testing.T) {
|
||||
server := BirdServer{
|
||||
t: t,
|
||||
expectedQuery: "show protocols",
|
||||
response: "Mock Response\nSecond Line",
|
||||
injectError: "eof",
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.birdSocket = server.socket
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape("show protocols"), nil)
|
||||
w := httptest.NewRecorder()
|
||||
birdHandler(w, r)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "Mock Response\nEOF")
|
||||
}
|
27
proxy/go.mod
27
proxy/go.mod
@ -1,28 +1,5 @@
|
||||
module github.com/xddxdd/bird-lg-go/proxy
|
||||
|
||||
go 1.17
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/magiconair/properties v1.8.7
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.16.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
require github.com/gorilla/handlers v1.5.1
|
||||
|
1758
proxy/go.sum
1758
proxy/go.sum
File diff suppressed because it is too large
Load Diff
111
proxy/main.go
111
proxy/main.go
@ -1,8 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"flag"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@ -21,81 +20,71 @@ func invalidHandler(httpW http.ResponseWriter, httpR *http.Request) {
|
||||
httpW.Write([]byte("Invalid Request\n"))
|
||||
}
|
||||
|
||||
func hasAccess(remoteAddr string) bool {
|
||||
// setting.allowedNets will always have at least one element because of how it's defined
|
||||
if len(setting.allowedNets) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if !strings.Contains(remoteAddr, ":") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove port from IP and remove brackets that are around IPv6 addresses
|
||||
remoteAddr = remoteAddr[0:strings.LastIndex(remoteAddr, ":")]
|
||||
remoteAddr = strings.Trim(remoteAddr, "[]")
|
||||
|
||||
ipObject := net.ParseIP(remoteAddr)
|
||||
if ipObject == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, net := range setting.allowedNets {
|
||||
if net.Contains(ipObject) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Access handler, check to see if client IP in allowed nets, continue if it is, send to invalidHandler if not
|
||||
// Access handler, check to see if client IP in allowed IPs, continue if it is, send to invalidHandler if not
|
||||
func accessHandler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(httpW http.ResponseWriter, httpR *http.Request) {
|
||||
if hasAccess(httpR.RemoteAddr) {
|
||||
|
||||
// setting.allowedIPs will always have at least one element because of how it's defined
|
||||
if setting.allowedIPs[0] == "" {
|
||||
next.ServeHTTP(httpW, httpR)
|
||||
} else {
|
||||
invalidHandler(httpW, httpR)
|
||||
return
|
||||
}
|
||||
|
||||
IPPort := httpR.RemoteAddr
|
||||
|
||||
// Remove port from IP and remove brackets that are around IPv6 addresses
|
||||
requestIp := IPPort[0:strings.LastIndex(IPPort, ":")]
|
||||
requestIp = strings.Replace(requestIp, "[", "", -1)
|
||||
requestIp = strings.Replace(requestIp, "]", "", -1)
|
||||
|
||||
for _, allowedIP := range setting.allowedIPs {
|
||||
if requestIp == allowedIP {
|
||||
next.ServeHTTP(httpW, httpR)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
invalidHandler(httpW, httpR)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
type settingType struct {
|
||||
birdSocket string
|
||||
listen string
|
||||
allowedNets []*net.IPNet
|
||||
tr_bin string
|
||||
tr_flags []string
|
||||
tr_raw bool
|
||||
birdSocket string
|
||||
listen string
|
||||
allowedIPs []string
|
||||
}
|
||||
|
||||
var setting settingType
|
||||
|
||||
// Wrapper of tracer
|
||||
func main() {
|
||||
parseSettings()
|
||||
tracerouteAutodetect()
|
||||
|
||||
fmt.Printf("Listening on %s...\n", setting.listen)
|
||||
|
||||
var l net.Listener
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(setting.listen, "/") {
|
||||
// Delete existing socket file, ignore errors (will fail later anyway)
|
||||
os.Remove(setting.listen)
|
||||
l, err = net.Listen("unix", setting.listen)
|
||||
} else {
|
||||
listenAddr := setting.listen
|
||||
if !strings.Contains(listenAddr, ":") {
|
||||
listenAddr = ":" + listenAddr
|
||||
}
|
||||
l, err = net.Listen("tcp", listenAddr)
|
||||
// Prepare default socket paths, use environment variable if possible
|
||||
var settingDefault = settingType{
|
||||
"/var/run/bird/bird.ctl",
|
||||
":8000",
|
||||
[]string{""},
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if birdSocketEnv := os.Getenv("BIRD_SOCKET"); birdSocketEnv != "" {
|
||||
settingDefault.birdSocket = birdSocketEnv
|
||||
}
|
||||
if listenEnv := os.Getenv("BIRDLG_LISTEN"); listenEnv != "" {
|
||||
settingDefault.listen = listenEnv
|
||||
}
|
||||
if AllowedIPsEnv := os.Getenv("ALLOWED_IPS"); AllowedIPsEnv != "" {
|
||||
settingDefault.allowedIPs = strings.Split(AllowedIPsEnv, ",")
|
||||
}
|
||||
|
||||
// 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")
|
||||
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.")
|
||||
flag.Parse()
|
||||
|
||||
setting.birdSocket = *birdParam
|
||||
setting.listen = *listenParam
|
||||
setting.allowedIPs = strings.Split(*AllowedIPsParam, ",")
|
||||
|
||||
// Start HTTP server
|
||||
http.HandleFunc("/", invalidHandler)
|
||||
@ -103,5 +92,5 @@ func main() {
|
||||
http.HandleFunc("/bird6", birdHandler)
|
||||
http.HandleFunc("/traceroute", tracerouteHandler)
|
||||
http.HandleFunc("/traceroute6", tracerouteHandler)
|
||||
http.Serve(l, handlers.LoggingHandler(os.Stdout, accessHandler(http.DefaultServeMux)))
|
||||
http.ListenAndServe(*listenParam, handlers.LoggingHandler(os.Stdout, accessHandler(http.DefaultServeMux)))
|
||||
}
|
||||
|
@ -1,99 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func TestHasAccessNotConfigured(t *testing.T) {
|
||||
setting.allowedNets = []*net.IPNet{}
|
||||
assert.Equal(t, hasAccess("whatever"), true)
|
||||
}
|
||||
|
||||
func TestHasAccessAllowIPv4(t *testing.T) {
|
||||
_, netip, _ := net.ParseCIDR("1.2.3.4/32")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
assert.Equal(t, hasAccess("1.2.3.4:4321"), true)
|
||||
}
|
||||
|
||||
func TestHasAccessAllowIPv4Net(t *testing.T) {
|
||||
_, netip, _ := net.ParseCIDR("1.2.3.0/24")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
assert.Equal(t, hasAccess("1.2.3.4:4321"), true)
|
||||
}
|
||||
|
||||
func TestHasAccessDenyIPv4(t *testing.T) {
|
||||
_, netip, _ := net.ParseCIDR("4.3.2.1/32")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
assert.Equal(t, hasAccess("1.2.3.4:4321"), false)
|
||||
}
|
||||
|
||||
func TestHasAccessAllowIPv6(t *testing.T) {
|
||||
_, netip, _ := net.ParseCIDR("2001:db8::1/128")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), true)
|
||||
}
|
||||
|
||||
func TestHasAccessAllowIPv6Net(t *testing.T) {
|
||||
_, netip, _ := net.ParseCIDR("2001:db8::/64")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), true)
|
||||
}
|
||||
|
||||
func TestHasAccessAllowIPv6DifferentForm(t *testing.T) {
|
||||
_, netip, _ := net.ParseCIDR("2001:db8::1/128")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), true)
|
||||
}
|
||||
|
||||
func TestHasAccessDenyIPv6(t *testing.T) {
|
||||
_, netip, _ := net.ParseCIDR("2001:db8::2/128")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), false)
|
||||
}
|
||||
|
||||
func TestHasAccessBadClientIP(t *testing.T) {
|
||||
_, netip, _ := net.ParseCIDR("1.2.3.4/32")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
assert.Equal(t, hasAccess("not an IP"), false)
|
||||
}
|
||||
|
||||
func TestHasAccessBadClientIPPort(t *testing.T) {
|
||||
_, netip, _ := net.ParseCIDR("1.2.3.4/32")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
assert.Equal(t, hasAccess("not an IP:not a port"), false)
|
||||
}
|
||||
|
||||
func TestAccessHandlerAllow(t *testing.T) {
|
||||
baseHandler := http.NotFoundHandler()
|
||||
wrappedHandler := accessHandler(baseHandler)
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/mock", nil)
|
||||
r.RemoteAddr = "1.2.3.4:4321"
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
_, netip, _ := net.ParseCIDR("1.2.3.4/32")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
|
||||
wrappedHandler.ServeHTTP(w, r)
|
||||
assert.Equal(t, w.Code, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAccessHandlerDeny(t *testing.T) {
|
||||
baseHandler := http.NotFoundHandler()
|
||||
wrappedHandler := accessHandler(baseHandler)
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/mock", nil)
|
||||
r.RemoteAddr = "1.2.3.4:4321"
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
_, netip, _ := net.ParseCIDR("4.3.2.1/32")
|
||||
setting.allowedNets = []*net.IPNet{netip}
|
||||
|
||||
wrappedHandler.ServeHTTP(w, r)
|
||||
assert.Equal(t, w.Code, http.StatusInternalServerError)
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type viperSettingType struct {
|
||||
BirdSocket string `mapstructure:"bird_socket"`
|
||||
Listen string `mapstructure:"listen"`
|
||||
AllowedNets string `mapstructure:"allowed_ips"`
|
||||
TracerouteBin string `mapstructure:"traceroute_bin"`
|
||||
TracerouteFlags string `mapstructure:"traceroute_flags"`
|
||||
TracerouteRaw bool `mapstructure:"traceroute_raw"`
|
||||
}
|
||||
|
||||
// Parse settings with viper, and convert to legacy setting format
|
||||
func parseSettings() {
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("/etc/bird-lg")
|
||||
viper.SetConfigName("bird-lgproxy")
|
||||
viper.AllowEmptyEnv(true)
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvPrefix("birdlg")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
|
||||
|
||||
// Legacy environment variables without prefixes
|
||||
viper.BindEnv("bird_socket", "BIRD_SOCKET")
|
||||
viper.BindEnv("listen", "BIRDLG_LISTEN", "BIRDLG_PROXY_PORT")
|
||||
viper.BindEnv("allowed_ips", "ALLOWED_IPS")
|
||||
|
||||
pflag.String("bird", "/var/run/bird/bird.ctl", "socket file for bird, set either in parameter or environment variable BIRD_SOCKET")
|
||||
viper.BindPFlag("bird_socket", pflag.Lookup("bird"))
|
||||
|
||||
pflag.String("listen", "8000", "listen address, set either in parameter or environment variable BIRDLG_PROXY_PORT")
|
||||
viper.BindPFlag("listen", pflag.Lookup("listen"))
|
||||
|
||||
pflag.String("allowed", "", "IPs or networks allowed to access this proxy, separated by commas. Don't set to allow all IPs.")
|
||||
viper.BindPFlag("allowed_ips", pflag.Lookup("allowed"))
|
||||
|
||||
pflag.String("traceroute_bin", "", "traceroute binary file, set either in parameter or environment variable BIRDLG_TRACEROUTE_BIN")
|
||||
viper.BindPFlag("traceroute_bin", pflag.Lookup("traceroute_bin"))
|
||||
|
||||
pflag.String("traceroute_flags", "", "traceroute flags, supports multiple flags separated with space.")
|
||||
viper.BindPFlag("traceroute_flags", pflag.Lookup("traceroute_flags"))
|
||||
|
||||
pflag.Bool("traceroute_raw", false, "whether to display traceroute outputs raw; set via parameter or environment variable BIRDLG_TRACEROUTE_RAW")
|
||||
viper.BindPFlag("traceroute_raw", pflag.Lookup("traceroute_raw"))
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
println("Warning on reading config: " + err.Error())
|
||||
}
|
||||
|
||||
viperSettings := viperSettingType{}
|
||||
if err := viper.Unmarshal(&viperSettings); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
setting.birdSocket = viperSettings.BirdSocket
|
||||
setting.listen = viperSettings.Listen
|
||||
|
||||
if viperSettings.AllowedNets != "" {
|
||||
for _, arg := range strings.Split(viperSettings.AllowedNets, ",") {
|
||||
|
||||
// if argument is an IP address, convert to CIDR by adding a suitable mask
|
||||
if !strings.Contains(arg, "/") {
|
||||
if strings.Contains(arg, ":") {
|
||||
// IPv6 address with /128 mask
|
||||
arg += "/128"
|
||||
} else {
|
||||
// IPv4 address with /32 mask
|
||||
arg += "/32"
|
||||
}
|
||||
}
|
||||
|
||||
// parse the network
|
||||
_, netip, err := net.ParseCIDR(arg)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to parse CIDR %s: %s\n", arg, err.Error())
|
||||
continue
|
||||
}
|
||||
setting.allowedNets = append(setting.allowedNets, netip)
|
||||
|
||||
}
|
||||
} else {
|
||||
setting.allowedNets = []*net.IPNet{}
|
||||
}
|
||||
|
||||
var err error
|
||||
setting.tr_bin = viperSettings.TracerouteBin
|
||||
setting.tr_flags, err = shlex.Split(viperSettings.TracerouteFlags)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
setting.tr_raw = viperSettings.TracerouteRaw
|
||||
|
||||
fmt.Printf("%#v\n", setting)
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseSettings(t *testing.T) {
|
||||
parseSettings()
|
||||
// Good as long as it doesn't panic
|
||||
}
|
@ -5,125 +5,76 @@ import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/shlex"
|
||||
)
|
||||
|
||||
func tracerouteArgsToString(cmd string, args []string, target []string) string {
|
||||
var cmdCombined = append([]string{cmd}, args...)
|
||||
cmdCombined = append(cmdCombined, target...)
|
||||
return strings.Join(cmdCombined, " ")
|
||||
}
|
||||
func tracerouteTryExecute(cmd []string, args [][]string) ([]byte, string) {
|
||||
var output []byte
|
||||
var errString = ""
|
||||
for i := range cmd {
|
||||
var err error
|
||||
var cmdCombined = cmd[i] + " " + strings.Join(args[i], " ")
|
||||
|
||||
func tracerouteTryExecute(cmd string, args []string, target []string) ([]byte, error) {
|
||||
instance := exec.Command(cmd, append(args, target...)...)
|
||||
output, err := instance.CombinedOutput()
|
||||
if err == nil {
|
||||
return output, nil
|
||||
}
|
||||
|
||||
return output, err
|
||||
}
|
||||
|
||||
func tracerouteDetect(cmd string, args []string) bool {
|
||||
target := []string{"127.0.0.1"}
|
||||
success := false
|
||||
if result, err := tracerouteTryExecute(cmd, args, target); err == nil {
|
||||
setting.tr_bin = cmd
|
||||
setting.tr_flags = args
|
||||
success = true
|
||||
fmt.Printf("Traceroute autodetect success: %s\n", tracerouteArgsToString(cmd, args, target))
|
||||
} else {
|
||||
fmt.Printf("Traceroute autodetect fail, continuing: %s (%s)\n%s", tracerouteArgsToString(cmd, args, target), err.Error(), result)
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
func tracerouteAutodetect() {
|
||||
if setting.tr_bin != "" && setting.tr_flags != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Traceroute (custom binary)
|
||||
if setting.tr_bin != "" {
|
||||
if tracerouteDetect(setting.tr_bin, []string{"-q1", "-N32", "-w1"}) {
|
||||
return
|
||||
}
|
||||
if tracerouteDetect(setting.tr_bin, []string{"-q1", "-w1"}) {
|
||||
return
|
||||
}
|
||||
if tracerouteDetect(setting.tr_bin, []string{}) {
|
||||
return
|
||||
instance := exec.Command(cmd[i], args[i]...)
|
||||
output, err = instance.CombinedOutput()
|
||||
if err == nil {
|
||||
return output, ""
|
||||
}
|
||||
errString += fmt.Sprintf("+ (Try %d) %s\n%s\n\n", (i + 1), cmdCombined, output)
|
||||
}
|
||||
|
||||
// MTR
|
||||
if tracerouteDetect("mtr", []string{"-w", "-c1", "-Z1", "-G1", "-b"}) {
|
||||
return
|
||||
}
|
||||
|
||||
// Traceroute
|
||||
if tracerouteDetect("traceroute", []string{"-q1", "-N32", "-w1"}) {
|
||||
return
|
||||
}
|
||||
if tracerouteDetect("traceroute", []string{"-q1", "-w1"}) {
|
||||
return
|
||||
}
|
||||
if tracerouteDetect("traceroute", []string{}) {
|
||||
return
|
||||
}
|
||||
|
||||
// Unsupported
|
||||
setting.tr_bin = ""
|
||||
setting.tr_flags = nil
|
||||
println("Traceroute autodetect failed! Traceroute will be disabled")
|
||||
return nil, errString
|
||||
}
|
||||
|
||||
func tracerouteHandler(httpW http.ResponseWriter, httpR *http.Request) {
|
||||
query := string(httpR.URL.Query().Get("q"))
|
||||
query = strings.TrimSpace(query)
|
||||
|
||||
if query == "" {
|
||||
invalidHandler(httpW, httpR)
|
||||
} 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 errString string
|
||||
skippedCounter := 0
|
||||
|
||||
if setting.tr_bin == "" {
|
||||
if runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" || runtime.GOOS == "openbsd" {
|
||||
result, errString = tracerouteTryExecute(
|
||||
[]string{
|
||||
"traceroute",
|
||||
},
|
||||
[][]string{
|
||||
{"-q1", "-w1", query},
|
||||
},
|
||||
)
|
||||
} else if runtime.GOOS == "linux" {
|
||||
result, errString = tracerouteTryExecute(
|
||||
[]string{
|
||||
"traceroute",
|
||||
"traceroute",
|
||||
},
|
||||
[][]string{
|
||||
{"-q1", "-N32", "-w1", query},
|
||||
{"-q1", "-w1", query},
|
||||
},
|
||||
)
|
||||
} else {
|
||||
httpW.WriteHeader(http.StatusInternalServerError)
|
||||
httpW.Write([]byte("traceroute not supported on this node.\n"))
|
||||
return
|
||||
}
|
||||
|
||||
result, err = tracerouteTryExecute(setting.tr_bin, setting.tr_flags, args)
|
||||
if err != nil {
|
||||
if errString != "" {
|
||||
httpW.WriteHeader(http.StatusInternalServerError)
|
||||
httpW.Write([]byte(fmt.Sprintf("Error executing traceroute: %s\n\n", err.Error())))
|
||||
httpW.Write([]byte(errString))
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
if setting.tr_raw {
|
||||
httpW.Write(result)
|
||||
} else {
|
||||
resultString := string(result)
|
||||
resultString = regexp.MustCompile(`(?m)^\s*(\d*)\s*\*\n`).ReplaceAllStringFunc(resultString, func(w string) string {
|
||||
skippedCounter++
|
||||
return ""
|
||||
})
|
||||
httpW.Write([]byte(strings.TrimSpace(resultString)))
|
||||
if skippedCounter > 0 {
|
||||
httpW.Write([]byte("\n\n" + strconv.Itoa(skippedCounter) + " hops not responding."))
|
||||
}
|
||||
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."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,168 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func TestTracerouteArgsToString(t *testing.T) {
|
||||
result := tracerouteArgsToString("traceroute", []string{
|
||||
"-a",
|
||||
"-b",
|
||||
"-c",
|
||||
}, []string{
|
||||
"google.com",
|
||||
})
|
||||
|
||||
assert.Equal(t, result, "traceroute -a -b -c google.com")
|
||||
}
|
||||
|
||||
func TestTracerouteTryExecuteSuccess(t *testing.T) {
|
||||
_, err := tracerouteTryExecute("sh", []string{
|
||||
"-c",
|
||||
}, []string{
|
||||
"true",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTracerouteTryExecuteFail(t *testing.T) {
|
||||
_, err := tracerouteTryExecute("sh", []string{
|
||||
"-c",
|
||||
}, []string{
|
||||
"false",
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Error("Should trigger error, not triggered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTracerouteDetectSuccess(t *testing.T) {
|
||||
result := tracerouteDetect("sh", []string{
|
||||
"-c",
|
||||
"true",
|
||||
})
|
||||
|
||||
assert.Equal(t, result, true)
|
||||
}
|
||||
|
||||
func TestTracerouteDetectFail(t *testing.T) {
|
||||
result := tracerouteDetect("sh", []string{
|
||||
"-c",
|
||||
"false",
|
||||
})
|
||||
|
||||
assert.Equal(t, result, false)
|
||||
}
|
||||
|
||||
func TestTracerouteAutodetect(t *testing.T) {
|
||||
pathBackup := os.Getenv("PATH")
|
||||
os.Setenv("PATH", "")
|
||||
defer os.Setenv("PATH", pathBackup)
|
||||
|
||||
setting.tr_bin = ""
|
||||
setting.tr_flags = []string{}
|
||||
tracerouteAutodetect()
|
||||
// Should not panic
|
||||
}
|
||||
|
||||
func TestTracerouteAutodetectExisting(t *testing.T) {
|
||||
setting.tr_bin = "mock"
|
||||
setting.tr_flags = []string{"mock"}
|
||||
tracerouteAutodetect()
|
||||
assert.Equal(t, setting.tr_bin, "mock")
|
||||
assert.Equal(t, setting.tr_flags, []string{"mock"})
|
||||
}
|
||||
|
||||
func TestTracerouteAutodetectFlagsOnly(t *testing.T) {
|
||||
pathBackup := os.Getenv("PATH")
|
||||
os.Setenv("PATH", "")
|
||||
defer os.Setenv("PATH", pathBackup)
|
||||
|
||||
setting.tr_bin = "mock"
|
||||
setting.tr_flags = nil
|
||||
tracerouteAutodetect()
|
||||
|
||||
// Should not panic
|
||||
}
|
||||
|
||||
func TestTracerouteHandlerWithoutQuery(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/traceroute", nil)
|
||||
w := httptest.NewRecorder()
|
||||
tracerouteHandler(w, r)
|
||||
assert.Equal(t, w.Code, http.StatusInternalServerError)
|
||||
if !strings.Contains(w.Body.String(), "Invalid Request") {
|
||||
t.Error("Did not get invalid request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTracerouteHandlerShlexError(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("\"1.1.1.1"), nil)
|
||||
w := httptest.NewRecorder()
|
||||
tracerouteHandler(w, r)
|
||||
assert.Equal(t, w.Code, http.StatusInternalServerError)
|
||||
if !strings.Contains(w.Body.String(), "parse") {
|
||||
t.Error("Did not get parsing error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTracerouteHandlerNoTracerouteFound(t *testing.T) {
|
||||
setting.tr_bin = ""
|
||||
setting.tr_flags = nil
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("1.1.1.1"), nil)
|
||||
w := httptest.NewRecorder()
|
||||
tracerouteHandler(w, r)
|
||||
assert.Equal(t, w.Code, http.StatusInternalServerError)
|
||||
if !strings.Contains(w.Body.String(), "not supported") {
|
||||
t.Error("Did not get not supported error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTracerouteHandlerExecuteError(t *testing.T) {
|
||||
setting.tr_bin = "sh"
|
||||
setting.tr_flags = []string{"-c", "false"}
|
||||
setting.tr_raw = true
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("1.1.1.1"), nil)
|
||||
w := httptest.NewRecorder()
|
||||
tracerouteHandler(w, r)
|
||||
assert.Equal(t, w.Code, http.StatusInternalServerError)
|
||||
if !strings.Contains(w.Body.String(), "Error executing traceroute") {
|
||||
t.Error("Did not get not execute error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTracerouteHandlerRaw(t *testing.T) {
|
||||
setting.tr_bin = "sh"
|
||||
setting.tr_flags = []string{"-c", "echo Mock"}
|
||||
setting.tr_raw = true
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("1.1.1.1"), nil)
|
||||
w := httptest.NewRecorder()
|
||||
tracerouteHandler(w, r)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "Mock\n")
|
||||
}
|
||||
|
||||
func TestTracerouteHandlerPostprocess(t *testing.T) {
|
||||
setting.tr_bin = "sh"
|
||||
setting.tr_flags = []string{"-c", "echo \"first line\n 2 *\nthird line\""}
|
||||
setting.tr_raw = false
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("1.1.1.1"), nil)
|
||||
w := httptest.NewRecorder()
|
||||
tracerouteHandler(w, r)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "first line\nthird line\n\n1 hops not responding.")
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user