You've already forked bird-lg-go
							
							
				mirror of
				https://github.com/xddxdd/bird-lg-go
				synced 2025-10-30 09:40:51 +01:00 
			
		
		
		
	Compare commits
	
		
			103 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1b2573d87c | ||
|   | 0d5337508b | ||
|   | b9094d3d6c | ||
|   | ec7f348418 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a632739443 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a9e278357a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e4c00c897f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4df3918b35 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 45dc24470d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 55ea5c3b28 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7eb44c3828 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 124fdedbda | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e6a98358b5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 761eb2160a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cc2a146a88 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3db9454350 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c30bed112c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | af5ab3c78f | ||
|   | 0fdde8afc7 | ||
|   | 39a129db9d | ||
|   | 0dd1c07b66 | ||
|   | f0f072c4a6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 657565857b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7ac2158e70 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 5c433bc27a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1b0b923da9 | ||
|   | 01438edaef | ||
|   | 90f36610dc | ||
|   | 6174208d07 | ||
|   | 76174cdc08 | ||
|   | 088bb6fe5a | ||
|   | 3951eed011 | ||
|   | 91c0a8962b | ||
|   | 5f7850a903 | ||
|   | 6a78cf2e80 | ||
|   | 5b5a44bcb6 | ||
|   | ac31862237 | ||
|   | 86129190ab | ||
|   | ff55064a20 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | dbb02c04ed | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c2b7de2e17 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c1b578e8db | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7b0e5689d4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3c46bda49d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 32e00d2ce3 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a19750cdef | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7f1cdaa4ee | ||
|   | 2d2193041e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | aad8ee98d7 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 00b5c12787 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 55a1eb54fd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0594edc69d | ||
|   | 38bf6aba09 | ||
|   | d261c22235 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 19aa8c77c5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | fe07ebb5a5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 66547ebfa9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d253e4311b | ||
|   | 026498ba2f | ||
|   | 27c348a864 | ||
|   | 43b4ad93dd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6176c45006 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 47113184f4 | ||
|   | 3c9a3e4339 | ||
|   | 8457b18d46 | ||
|   | f8f64b03a6 | ||
|   | cc818c1cc0 | ||
|   | 6224b43808 | ||
|   | 17e0b14243 | ||
|   | b4c1bed9ba | ||
|   | abb32abff3 | ||
|   | b368c75aa3 | ||
|   | 09405cdb38 | ||
|   | f999d47d9f | ||
|   | 005dfb1435 | ||
|   | 4bd7a6bb95 | ||
|   | 462d76a2d0 | ||
|   | 58f217578c | ||
|   | 0e95727de1 | ||
|   | a48f1c8040 | ||
|   | 81acde3a37 | ||
|   | 7c0fe0d512 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a5f4452d02 | ||
|   | b237185ef7 | ||
|   | e949646790 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | bb479d22ae | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d40f41b4d5 | ||
|   | cdc34704b5 | ||
|   | db58bd3354 | ||
|   | a0246ccee2 | ||
|   | ccd14af0c8 | ||
|   | 594ca80f50 | ||
|   | 5625058e71 | ||
|   | 7efa3237a9 | ||
|   | 7b0c8c0556 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ffd9165062 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 24fd5203e8 | ||
|   | 49a05767c1 | ||
|   | e7010f75f8 | ||
|   | dba2af7634 | ||
|   | 049775319b | ||
|   | 47c66b125c | ||
|   | 9e17b116f1 | 
| @@ -1,127 +0,0 @@ | ||||
| version: 2.1 | ||||
|  | ||||
| workflows: | ||||
|   docker: | ||||
|     jobs: | ||||
|       - build | ||||
|       - docker-frontend-deploy: | ||||
|           context: | ||||
|             - docker | ||||
|           requires: | ||||
|             - build | ||||
|           filters: | ||||
|             branches: | ||||
|               only: master | ||||
|       - docker-proxy-deploy: | ||||
|           context: | ||||
|             - docker | ||||
|           requires: | ||||
|             - build | ||||
|           filters: | ||||
|             branches: | ||||
|               only: master | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     docker: | ||||
|       - image: cimg/go:1.17 | ||||
|     working_directory: /home/circleci/go/src/github.com/xddxdd/bird-lg-go | ||||
|     steps: | ||||
|       - checkout | ||||
|       - run:  | ||||
|           name: Test frontend | ||||
|           command: | | ||||
|             export GO111MODULE=on | ||||
|             cd frontend | ||||
|             go get -v -t -d ./... | ||||
|             go test -v ./... | ||||
|       - run:  | ||||
|           name: Test proxy | ||||
|           command: | | ||||
|             export GO111MODULE=on | ||||
|             cd proxy | ||||
|             go get -v -t -d ./... | ||||
|             go test -v ./... | ||||
|  | ||||
|   docker-frontend-deploy: | ||||
|     machine: | ||||
|       image: ubuntu-2004:202111-02 | ||||
|     environment: | ||||
|       BUILDX_PLATFORMS: linux/amd64,linux/arm64,linux/386,linux/arm/v7 | ||||
|     steps: | ||||
|       - checkout | ||||
|       - run: | ||||
|           name: Install buildx | ||||
|           command: | | ||||
|             BUILDX_BINARY_URL="https://github.com/docker/buildx/releases/download/v0.7.1/buildx-v0.7.1.linux-amd64" | ||||
|  | ||||
|             curl --output docker-buildx \ | ||||
|               --silent --show-error --location --fail --retry 3 \ | ||||
|               "$BUILDX_BINARY_URL" | ||||
|  | ||||
|             mkdir -p ~/.docker/cli-plugins | ||||
|  | ||||
|             mv docker-buildx ~/.docker/cli-plugins/ | ||||
|             chmod a+x ~/.docker/cli-plugins/docker-buildx | ||||
|  | ||||
|             docker buildx install | ||||
|             # Run binfmt | ||||
|             docker run --rm --privileged multiarch/qemu-user-static --reset -p yes | ||||
|       - run: | ||||
|           name: Build Docker image | ||||
|           environment: | ||||
|             BUILD_ID: << pipeline.number >> | ||||
|           command: | | ||||
|             echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin | ||||
|             docker buildx create --name mybuilder --use | ||||
|             docker buildx build \ | ||||
|               --platform $BUILDX_PLATFORMS \ | ||||
|               -t $DOCKER_USERNAME/bird-lg-go:circleci-build$BUILD_ID \ | ||||
|               --progress plain \ | ||||
|               --push frontend | ||||
|             docker buildx build \ | ||||
|               --platform $BUILDX_PLATFORMS \ | ||||
|               -t $DOCKER_USERNAME/bird-lg-go:latest \ | ||||
|               --progress plain \ | ||||
|               --push frontend | ||||
|  | ||||
|   docker-proxy-deploy: | ||||
|     machine: | ||||
|       image: ubuntu-2004:202111-02 | ||||
|     environment: | ||||
|       BUILDX_PLATFORMS: linux/amd64,linux/arm64,linux/386,linux/arm/v7 | ||||
|     steps: | ||||
|       - checkout | ||||
|       - run: | ||||
|           name: Install buildx | ||||
|           command: | | ||||
|             BUILDX_BINARY_URL="https://github.com/docker/buildx/releases/download/v0.7.1/buildx-v0.7.1.linux-amd64" | ||||
|  | ||||
|             curl --output docker-buildx \ | ||||
|               --silent --show-error --location --fail --retry 3 \ | ||||
|               "$BUILDX_BINARY_URL" | ||||
|  | ||||
|             mkdir -p ~/.docker/cli-plugins | ||||
|  | ||||
|             mv docker-buildx ~/.docker/cli-plugins/ | ||||
|             chmod a+x ~/.docker/cli-plugins/docker-buildx | ||||
|  | ||||
|             docker buildx install | ||||
|             # Run binfmt | ||||
|             docker run --rm --privileged multiarch/qemu-user-static --reset -p yes | ||||
|       - run: | ||||
|           name: Build Docker image | ||||
|           environment: | ||||
|             BUILD_ID: << pipeline.number >> | ||||
|           command: | | ||||
|             echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin | ||||
|             docker buildx create --name mybuilder --use | ||||
|             docker buildx build \ | ||||
|               --platform $BUILDX_PLATFORMS \ | ||||
|               -t $DOCKER_USERNAME/bird-lgproxy-go:circleci-build$BUILD_ID \ | ||||
|               --push proxy | ||||
|             docker buildx build \ | ||||
|               --platform $BUILDX_PLATFORMS \ | ||||
|               -t $DOCKER_USERNAME/bird-lgproxy-go:latest \ | ||||
|               --progress plain \ | ||||
|               --push proxy | ||||
							
								
								
									
										16
									
								
								.github/workflows/auto-merge.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.github/workflows/auto-merge.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| name: auto-merge | ||||
|  | ||||
| on: | ||||
|   pull_request_target: | ||||
|  | ||||
| jobs: | ||||
|   auto-merge: | ||||
|     name: Dependabot Auto Merge | ||||
|     runs-on: ubuntu-latest | ||||
|     if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: ahmadnassri/action-dependabot-auto-merge@v2 | ||||
|         with: | ||||
|           target: minor | ||||
|           github-token: ${{ secrets.AUTOMERGE_TOKEN }} | ||||
							
								
								
									
										127
									
								
								.github/workflows/develop.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								.github/workflows/develop.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - 'master' | ||||
|   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 -I 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 | ||||
|  | ||||
|       - name: Test mtr binary in proxy image | ||||
|         run: | | ||||
|           docker build -t local/proxy:mtr -f proxy/Dockerfile.mtr proxy/ | ||||
|           docker run --rm --net host --entrypoint mtr local/proxy:mtr -w -c1 -Z1 -G1 -b 127.0.0.1 || exit 1 | ||||
|           docker run --rm --net host --entrypoint mtr local/proxy:mtr -w -c1 -Z1 -G1 -b ::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 }} | ||||
|  | ||||
|       - name: Build proxy docker image | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           context: '{{defaultContext}}:proxy' | ||||
|           file: 'Dockerfile.mtr' | ||||
|           platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7 | ||||
|           push: true | ||||
|           tags: | | ||||
|             xddxdd/bird-lgproxy-go:develop-mtr | ||||
|             xddxdd/bird-lgproxy-go:develop-${{ github.sha }}-mtr | ||||
|             ghcr.io/xddxdd/bird-lg-go:proxy-develop-mtr | ||||
|             ghcr.io/xddxdd/bird-lg-go:proxy-develop-${{ github.sha }}-mtr | ||||
							
								
								
									
										73
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										73
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -3,10 +3,11 @@ on: | ||||
|     types: [created] | ||||
|  | ||||
| jobs: | ||||
|   releases-matrix: | ||||
|   go-release: | ||||
|     name: Release Go Binary | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         goos: [linux, windows, darwin] | ||||
|         goarch: ["386", amd64, "arm", arm64] | ||||
| @@ -18,18 +19,82 @@ jobs: | ||||
|           - goarch: "arm" | ||||
|             goos: windows | ||||
|     steps: | ||||
|     - uses: actions/checkout@v3 | ||||
|     - uses: wangyoucao577/go-release-action@v1.30 | ||||
|     - 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" | ||||
|     - uses: wangyoucao577/go-release-action@v1.30 | ||||
|  | ||||
|     - 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 }} | ||||
|  | ||||
|       - name: Build proxy docker image | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           context: '{{defaultContext}}:proxy' | ||||
|           file: 'Dockerfile.mtr' | ||||
|           platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7 | ||||
|           push: true | ||||
|           tags: | | ||||
|             xddxdd/bird-lgproxy-go:latest-mtr | ||||
|             xddxdd/bird-lgproxy-go:${{ github.event.release.tag_name }}-mtr | ||||
|             ghcr.io/xddxdd/bird-lg-go:proxy-mtr | ||||
|             ghcr.io/xddxdd/bird-lg-go:proxy-${{ github.event.release.tag_name }}-mtr | ||||
|   | ||||
							
								
								
									
										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.* | ||||
							
								
								
									
										35
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								README.md
									
									
									
									
									
								
							| @@ -58,8 +58,6 @@ Configuration can be set in: | ||||
|  | ||||
| Configuration is handled by [viper](https://github.com/spf13/viper), any config format supported by it can be used. | ||||
|  | ||||
| > Note: the config system is replaced with viper only recently (2022-07-08). If some config items do not work, please open an issue, and use commit [892a7bee22a1bb02d3b4da6d270c65b6e4e1321a](https://github.com/xddxdd/bird-lg-go/tree/892a7bee22a1bb02d3b4da6d270c65b6e4e1321a) (last version before config system replace) for the time being. | ||||
|  | ||||
| | Config Key | Parameter | Environment Variable | Description | | ||||
| | ---------- | --------- | -------------------- | ----------- | | ||||
| | servers | --servers | BIRDLG_SERVERS | server name prefixes, separated by comma | | ||||
| @@ -79,6 +77,8 @@ Configuration is handled by [viper](https://github.com/spf13/viper), any config | ||||
| | 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 | ||||
|  | ||||
| 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 | ||||
| @@ -90,7 +90,8 @@ Example: the following docker-compose.yml entry does the same as above, but by s | ||||
| ```yaml | ||||
| services: | ||||
|   bird-lg: | ||||
|     image: xddxdd/bird-lg-go | ||||
|     # 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: | ||||
| @@ -122,16 +123,32 @@ Configuration can be set in: | ||||
|  | ||||
| Configuration is handled by [viper](https://github.com/spf13/viper), any config format supported by it can be used. | ||||
|  | ||||
| > Note: the config system is replaced with viper only recently (2022-07-08). If some config items do not work, please open an issue, and use commit [892a7bee22a1bb02d3b4da6d270c65b6e4e1321a](https://github.com/xddxdd/bird-lg-go/tree/892a7bee22a1bb02d3b4da6d270c65b6e4e1321a) (last version before config system replace) for the time being. | ||||
|  | ||||
| | Config Key | Parameter | Environment Variable | Description | | ||||
| | ---------- | --------- | -------------------- | ----------- | | ||||
| | allowed_ips | --allowed | ALLOWED_IPS | IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") | | ||||
| | 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(default "traceroute") | | ||||
| | 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 | ||||
|  | ||||
| Example: start proxy with default configuration, should work "out of the box" on Debian 9 with BIRDv1: | ||||
|  | ||||
| ```bash | ||||
| @@ -149,7 +166,9 @@ Example: the following docker-compose.yml entry does the same as above, but by s | ||||
| ```yaml | ||||
| services: | ||||
|   bird-lgproxy: | ||||
|     image: xddxdd/bird-lgproxy-go | ||||
|     # Use xddxdd/bird-lgproxy-go:develop for the latest build from master branch | ||||
|     # Use xddxdd/bird-lgproxy-go:latest-mtr to use MTR instead of Traceroute | ||||
|     image: xddxdd/bird-lgproxy-go:latest | ||||
|     container_name: bird-lgproxy | ||||
|     restart: always | ||||
|     volumes: | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM golang:buster AS step_0 | ||||
| FROM golang AS step_0 | ||||
| ENV CGO_ENABLED=0 GO111MODULE=on | ||||
| WORKDIR /root | ||||
| COPY . . | ||||
| @@ -6,6 +6,28 @@ RUN go build -ldflags "-w -s" -o /frontend | ||||
|  | ||||
| ################################################################################ | ||||
|  | ||||
| FROM scratch AS step_1 | ||||
| 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 | ||||
| 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"] | ||||
|   | ||||
| @@ -20,6 +20,7 @@ type apiGenericResultPair struct { | ||||
| type apiSummaryResultPair struct { | ||||
| 	Server string           `json:"server"` | ||||
| 	Data   []SummaryRowData `json:"data"` | ||||
| 	Error  string           `json:"error,omitempty"` | ||||
| } | ||||
|  | ||||
| type apiResponse struct { | ||||
| @@ -70,9 +71,12 @@ func apiSummaryHandler(request apiRequest) 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:   []SummaryRowData{}, | ||||
| 				Error:  err.Error(), | ||||
| 			}) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		response.Result = append(response.Result, &apiSummaryResultPair{ | ||||
| @@ -113,7 +117,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { | ||||
| 	} else { | ||||
| 		handler := apiHandlerMap[request.Type] | ||||
| 		if handler == nil { | ||||
| 			response = apiErrorHandler(errors.New("Invalid request type")) | ||||
| 			response = apiErrorHandler(errors.New("invalid request type")) | ||||
| 		} else { | ||||
| 			response = handler(request) | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										210
									
								
								frontend/api_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								frontend/api_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | ||||
| 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, "") | ||||
|  | ||||
| 	summary := response.Result[0].(*apiSummaryResultPair) | ||||
| 	assert.Equal(t, summary.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) | ||||
| } | ||||
							
								
								
									
										83
									
								
								frontend/asn_cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								frontend/asn_cache.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| 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 | ||||
| } | ||||
							
								
								
									
										52
									
								
								frontend/asn_cache_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								frontend/asn_cache_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| 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") | ||||
| } | ||||
| @@ -5,8 +5,19 @@ | ||||
| <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> | ||||
|   function decodeBase64(base64) { | ||||
|     const text = atob(base64); | ||||
|     const length = text.length; | ||||
|     const bytes = new Uint8Array(length); | ||||
|     for (let i = 0; i < length; i++) { | ||||
|         bytes[i] = text.charCodeAt(i); | ||||
|     } | ||||
|     const decoder = new TextDecoder(); | ||||
|     return decoder.decode(bytes); | ||||
|   } | ||||
|  | ||||
|   var viz = new Viz(); | ||||
|   viz.renderSVGElement(atob({{ .Result }})) | ||||
|   viz.renderSVGElement(decodeBase64({{ .Result }})) | ||||
|   .then(element => { | ||||
|     document.getElementById("bgpmap").appendChild(element); | ||||
|   }) | ||||
|   | ||||
| @@ -7,7 +7,20 @@ | ||||
| <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"> | ||||
| <link rel="stylesheet" href="/static/jsdelivr/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" crossorigin="anonymous"> | ||||
| <style> | ||||
| .navbar-nav { | ||||
| 	flex-wrap: wrap; | ||||
| } | ||||
| @media (min-width: 768px) { | ||||
| 	.navbar form { | ||||
| 		min-width: 400px; | ||||
| 	} | ||||
| 	.nav-link { | ||||
| 		padding: 0.2rem 0.5rem !important; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
| <meta name="robots" content="noindex, nofollow"> | ||||
| </head> | ||||
| <body> | ||||
| @@ -60,7 +73,7 @@ | ||||
| 					<option value="{{ html $k }}"{{ if eq $k $.URLOption }} selected{{end}}>{{ html $v }}</option> | ||||
| 					{{ end }} | ||||
| 				</select> | ||||
| 				<input name="server" class="d-none" value="{{ html $server }}"> | ||||
| 				<input name="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> | ||||
| @@ -74,8 +87,8 @@ | ||||
| 	{{ .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/jsdelivr/npm/jquery@3.5.1/dist/jquery.min.js" crossorigin="anonymous"></script> | ||||
| <script src="/static/jsdelivr/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" crossorigin="anonymous"></script> | ||||
| <script src="/static/sortTable.js"></script> | ||||
|  | ||||
| <script> | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										178
									
								
								frontend/bgpmap_graph.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								frontend/bgpmap_graph.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"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 { | ||||
| 	buffer := &bytes.Buffer{} | ||||
| 	encoder := json.NewEncoder(buffer) | ||||
| 	encoder.SetEscapeHTML(false) | ||||
| 	err := encoder.Encode(s) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return err.Error() | ||||
| 	} else { | ||||
| 		return string(buffer.Bytes()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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,71 +1,23 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| 	"path" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestGetASNRepresentationDNS(t *testing.T) { | ||||
| 	checkNetwork(t) | ||||
| func readDataFile(t *testing.T, filename string) string { | ||||
| 	_, sourceName, _, _ := runtime.Caller(0) | ||||
| 	projectRoot := path.Join(path.Dir(sourceName), "..") | ||||
| 	dir := path.Join(projectRoot, filename) | ||||
|  | ||||
| 	setting.dnsInterface = "asn.cymru.com" | ||||
| 	setting.whoisServer = "" | ||||
| 	result := getASNRepresentation("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" | ||||
| 	result := getASNRepresentation("6939") | ||||
| 	if !strings.Contains(result, "HURRICANE") { | ||||
| 		t.Errorf("Lookup AS6939 failed, got %s", result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetASNRepresentationFallback(t *testing.T) { | ||||
| 	setting.dnsInterface = "" | ||||
| 	setting.whoisServer = "" | ||||
| 	result := getASNRepresentation("6939") | ||||
| 	if result != "AS6939" { | ||||
| 		t.Errorf("Lookup AS6939 failed, got %s", result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestBirdRouteToGraphviz(t *testing.T) { | ||||
| 	setting.dnsInterface = "" | ||||
|  | ||||
| 	// Don't change formatting of the following strings! | ||||
|  | ||||
| 	fakeResult := `192.168.0.1/32       unicast [alpha 2021-01-14 from 192.168.0.2] * (100) [AS12345i] | ||||
| 	via 192.168.0.2 on eth0 | ||||
| 	Type: BGP univ | ||||
| 	BGP.origin: IGP | ||||
| 	BGP.as_path: 4242422601 | ||||
| 	BGP.next_hop: 172.18.0.2` | ||||
|  | ||||
| 	expectedResult := `digraph { | ||||
| "Target: 192.168.0.1" [color=red,shape=diamond]; | ||||
| "alpha" [color=blue,shape=box]; | ||||
| "alpha" -> "AS4242422601" [fontsize=12.0,color=red,label="alpha*\n172.18.0.2"]; | ||||
| "AS4242422601" -> "Target: 192.168.0.1" [color=red]; | ||||
| }` | ||||
|  | ||||
| 	result := birdRouteToGraphviz([]string{ | ||||
| 		"alpha", | ||||
| 	}, []string{ | ||||
| 		fakeResult, | ||||
| 	}, "192.168.0.1") | ||||
|  | ||||
| 	for _, line := range strings.Split(result, "\n") { | ||||
| 		if !strings.Contains(expectedResult, line) { | ||||
| 			t.Errorf("Unexpected line in result: %s", line) | ||||
| 		} | ||||
| 	data, err := ioutil.ReadFile(dir) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	return string(data) | ||||
| } | ||||
|  | ||||
| func TestBirdRouteToGraphvizXSS(t *testing.T) { | ||||
| @@ -81,7 +33,52 @@ func TestBirdRouteToGraphvizXSS(t *testing.T) { | ||||
| 		fakeResult, | ||||
| 	}, fakeResult) | ||||
|  | ||||
| 	if strings.Contains(result, "<script>") { | ||||
| 	if strings.Contains(result, fakeResult) { | ||||
| 		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") | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -65,7 +65,7 @@ func shortenWhoisFilter(whois string) string { | ||||
| 		shouldSkip := false | ||||
| 		shouldSkip = shouldSkip || len(s) == 0 | ||||
| 		shouldSkip = shouldSkip || len(s) > 0 && s[0] == '#' | ||||
| 		shouldSkip = shouldSkip || strings.Contains(strings.ToUpper(s), "REDACTED FOR PRIVACY") | ||||
| 		shouldSkip = shouldSkip || strings.Contains(strings.ToUpper(s), "REDACTED") | ||||
|  | ||||
| 		if shouldSkip { | ||||
| 			skippedLinesLonger++ | ||||
|   | ||||
| @@ -28,3 +28,79 @@ func TestDN42WhoisFilterUnneeded(t *testing.T) { | ||||
| 		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,10 +1,33 @@ | ||||
| module github.com/xddxdd/bird-lg-go/frontend | ||||
|  | ||||
| go 1.16 | ||||
| go 1.17 | ||||
|  | ||||
| require ( | ||||
| 	github.com/felixge/httpsnoop v1.0.3 // indirect | ||||
| 	github.com/gorilla/handlers v1.5.1 | ||||
| 	github.com/spf13/pflag v1.0.5 | ||||
| 	github.com/spf13/viper v1.14.0 | ||||
| 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 | ||||
| 	github.com/gorilla/handlers v1.5.2 | ||||
| 	github.com/jarcoal/httpmock v1.4.0 | ||||
| 	github.com/magiconair/properties v1.8.10 | ||||
| 	github.com/spf13/pflag v1.0.6 | ||||
| 	github.com/spf13/viper v1.19.0 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/felixge/httpsnoop v1.0.4 // indirect | ||||
| 	github.com/fsnotify/fsnotify v1.7.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.2.2 // indirect | ||||
| 	github.com/sagikazarmark/locafero v0.4.0 // indirect | ||||
| 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect | ||||
| 	github.com/sourcegraph/conc v0.3.0 // indirect | ||||
| 	github.com/spf13/afero v1.11.0 // indirect | ||||
| 	github.com/spf13/cast v1.6.0 // indirect | ||||
| 	github.com/subosito/gotenv v1.6.0 // indirect | ||||
| 	go.uber.org/atomic v1.9.0 // indirect | ||||
| 	go.uber.org/multierr v1.9.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect | ||||
| 	golang.org/x/sys v0.18.0 // indirect | ||||
| 	golang.org/x/text v0.14.0 // indirect | ||||
| 	gopkg.in/ini.v1 v1.67.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										1896
									
								
								frontend/go.sum
									
									
									
									
									
								
							
							
						
						
									
										1896
									
								
								frontend/go.sum
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -2,11 +2,14 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"io" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/jarcoal/httpmock" | ||||
| ) | ||||
|  | ||||
| type channelData struct { | ||||
| @@ -14,6 +17,29 @@ type channelData struct { | ||||
| 	data string | ||||
| } | ||||
|  | ||||
| func createConnectionTimeoutRoundTripper(timeout int) http.RoundTripper { | ||||
| 	context := net.Dialer{ | ||||
| 		Timeout: time.Duration(timeout) * time.Second, | ||||
| 	} | ||||
|  | ||||
| 	// Prefer httpmock's transport if activated, so unit tests can work | ||||
| 	if http.DefaultTransport == httpmock.DefaultTransport { | ||||
| 		return httpmock.DefaultTransport | ||||
| 	} | ||||
|  | ||||
| 	return &http.Transport{ | ||||
| 		DialContext: context.DialContext, | ||||
|  | ||||
| 		// Default options from transport.go | ||||
| 		Proxy:                 http.ProxyFromEnvironment, | ||||
| 		ForceAttemptHTTP2:     true, | ||||
| 		MaxIdleConns:          100, | ||||
| 		IdleConnTimeout:       90 * time.Second, | ||||
| 		TLSHandshakeTimeout:   10 * time.Second, | ||||
| 		ExpectContinueTimeout: 1 * time.Second, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Send commands to lgproxy instances in parallel, and retrieve their responses | ||||
| func batchRequest(servers []string, endpoint string, command string) []string { | ||||
| 	// Channel and array for storing responses | ||||
| @@ -47,7 +73,10 @@ func batchRequest(servers []string, endpoint string, command string) []string { | ||||
| 			} | ||||
| 			url := "http://" + hostname + ":" + 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} | ||||
| 				client := http.Client{ | ||||
| 					Transport: createConnectionTimeoutRoundTripper(setting.connectionTimeOut), | ||||
| 					Timeout:   time.Duration(setting.timeOut) * time.Second, | ||||
| 				} | ||||
| 				response, err := client.Get(url) | ||||
| 				if err != nil { | ||||
| 					ch <- channelData{i, "request failed: " + err.Error() + "\n"} | ||||
| @@ -56,8 +85,8 @@ func batchRequest(servers []string, endpoint string, command string) []string { | ||||
|  | ||||
| 				buf := make([]byte, 65536) | ||||
| 				n, err := io.ReadFull(response.Body, buf) | ||||
| 				if err != nil && err != io.ErrUnexpectedEOF { | ||||
| 					ch <- channelData{i, err.Error()} | ||||
| 				if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { | ||||
| 					ch <- channelData{i, "request failed: " + err.Error()} | ||||
| 				} else { | ||||
| 					ch <- channelData{i, string(buf[:n])} | ||||
| 				} | ||||
|   | ||||
							
								
								
									
										163
									
								
								frontend/lgproxy_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								frontend/lgproxy_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| 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") | ||||
| 	} | ||||
| } | ||||
| @@ -7,24 +7,26 @@ import ( | ||||
| ) | ||||
|  | ||||
| type settingType struct { | ||||
| 	servers         []string | ||||
| 	serversDisplay  []string | ||||
| 	domain          string | ||||
| 	proxyPort       int | ||||
| 	whoisServer     string | ||||
| 	listen          string | ||||
| 	dnsInterface    string | ||||
| 	netSpecificMode string | ||||
| 	titleBrand      string | ||||
| 	navBarBrand     string | ||||
| 	navBarBrandURL  string | ||||
| 	navBarAllServer string | ||||
| 	navBarAllURL    string | ||||
| 	bgpmapInfo      string | ||||
| 	telegramBotName string | ||||
| 	protocolFilter  []string | ||||
| 	nameFilter      string | ||||
| 	timeOut         int | ||||
| 	servers           []string | ||||
| 	serversDisplay    []string | ||||
| 	domain            string | ||||
| 	proxyPort         int | ||||
| 	whoisServer       string | ||||
| 	listen            string | ||||
| 	dnsInterface      string | ||||
| 	netSpecificMode   string | ||||
| 	titleBrand        string | ||||
| 	navBarBrand       string | ||||
| 	navBarBrandURL    string | ||||
| 	navBarAllServer   string | ||||
| 	navBarAllURL      string | ||||
| 	bgpmapInfo        string | ||||
| 	telegramBotName   string | ||||
| 	protocolFilter    []string | ||||
| 	nameFilter        string | ||||
| 	timeOut           int | ||||
| 	connectionTimeOut int | ||||
| 	trustProxyHeaders bool | ||||
| } | ||||
|  | ||||
| var setting settingType | ||||
|   | ||||
| @@ -193,6 +193,11 @@ func summaryParse(data string, serverName string) (TemplateSummary, error) { | ||||
| 			row.Info = strings.TrimSpace(lineSplitted[10]) | ||||
| 		} | ||||
|  | ||||
| 		// Dynamic BGP session, show without any color | ||||
| 		if strings.Contains(row.Info, "Passive") { | ||||
| 			row.MappedState = summaryStateMap["passive"] | ||||
| 		} | ||||
|  | ||||
| 		// add to the result | ||||
| 		args.Rows = append(args.Rows, row) | ||||
| 	} | ||||
|   | ||||
| @@ -8,6 +8,17 @@ import ( | ||||
| 	"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"} | ||||
| @@ -101,17 +112,8 @@ func TestSummaryTableXSS(t *testing.T) { | ||||
| func TestSummaryTableProtocolFilter(t *testing.T) { | ||||
| 	initSettings() | ||||
| 	setting.protocolFilter = []string{"Static", "Direct", "Babel"} | ||||
| 	data := `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    ` | ||||
|  | ||||
| 	result := string(summaryTable(data, "testserver")) | ||||
| 	result := string(summaryTable(BirdSummaryData, "testserver")) | ||||
| 	expectedInclude := []string{"static1", "static2", "int_babel", "direct1"} | ||||
| 	expectedExclude := []string{"device1", "kernel1", "kernel2"} | ||||
|  | ||||
| @@ -134,17 +136,8 @@ int_babel  Babel      ---        up     2021-08-27    ` | ||||
| func TestSummaryTableNameFilter(t *testing.T) { | ||||
| 	initSettings() | ||||
| 	setting.nameFilter = "^static" | ||||
| 	data := `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    ` | ||||
|  | ||||
| 	result := string(summaryTable(data, "testserver")) | ||||
| 	result := string(summaryTable(BirdSummaryData, "testserver")) | ||||
| 	expectedInclude := []string{"device1", "kernel1", "kernel2", "direct1", "int_babel"} | ||||
| 	expectedExclude := []string{"static1", "static2"} | ||||
|  | ||||
|   | ||||
| @@ -9,23 +9,25 @@ import ( | ||||
| ) | ||||
|  | ||||
| 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"` | ||||
| 	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"` | ||||
| 	ConnectionTimeOut int    `mapstructure:"connection_timeout"` | ||||
| 	TrustProxyHeaders bool   `mapstructure:"trust_proxy_headers"` | ||||
| } | ||||
|  | ||||
| // Parse settings with viper, and convert to legacy setting format | ||||
| @@ -33,6 +35,7 @@ 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("-", "_", ".", "_")) | ||||
| @@ -86,9 +89,15 @@ func parseSettings() { | ||||
| 	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") | ||||
| 	pflag.Int("time-out", 120, "time before backend HTTP request times out, in seconds; defaults to 120 if not set") | ||||
| 	viper.BindPFlag("timeout", pflag.Lookup("time-out")) | ||||
|  | ||||
| 	pflag.Int("connection-time-out", 5, "time before backend TCP connection times out, in seconds; defaults to 5 if not set") | ||||
| 	viper.BindPFlag("connection_timeout", pflag.Lookup("connection-time-out")) | ||||
|  | ||||
| 	pflag.Bool("trust-proxy-headers", false, "Trust X-Forwared-For, X-Real-IP, X-Forwarded-Proto, X-Forwarded-Scheme and X-Forwarded-Host sent by the client") | ||||
| 	viper.BindPFlag("trust_proxy_headers", pflag.Lookup("trust-proxy-headers")) | ||||
|  | ||||
| 	pflag.Parse() | ||||
|  | ||||
| 	if err := viper.ReadInConfig(); err != nil { | ||||
| @@ -138,6 +147,8 @@ func parseSettings() { | ||||
|  | ||||
| 	setting.nameFilter = viperSettings.NameFilter | ||||
| 	setting.timeOut = viperSettings.TimeOut | ||||
| 	setting.connectionTimeOut = viperSettings.ConnectionTimeOut | ||||
| 	setting.trustProxyHeaders = viperSettings.TrustProxyHeaders | ||||
|  | ||||
| 	fmt.Printf("%#v\n", setting) | ||||
| } | ||||
|   | ||||
							
								
								
									
										8
									
								
								frontend/settings_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/settings_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| package main | ||||
|  | ||||
| import "testing" | ||||
|  | ||||
| func TestParseSettings(t *testing.T) { | ||||
| 	parseSettings() | ||||
| 	// Good as long as it doesn't panic | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -3,6 +3,7 @@ package main | ||||
| import ( | ||||
| 	"embed" | ||||
| 	"html/template" | ||||
|         "net/url" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| @@ -104,6 +105,12 @@ var requiredTemplates = [...]string{ | ||||
| 	"bird", | ||||
| } | ||||
|  | ||||
| // define functions to be made available in templates | ||||
|  | ||||
| var funcMap = template.FuncMap{ | ||||
|         "pathescape": url.PathEscape, | ||||
| } | ||||
|  | ||||
| // import templates from embedded assets | ||||
|  | ||||
| func ImportTemplates() { | ||||
| @@ -121,7 +128,7 @@ func ImportTemplates() { | ||||
| 		} | ||||
|  | ||||
| 		// and add it to the template library | ||||
| 		template, err := template.New(tmpl).Parse(string(def)) | ||||
| 		template, err := template.New(tmpl).Funcs(funcMap).Parse(string(def)) | ||||
| 		if err != nil { | ||||
| 			panic("Unable to parse template (" + TEMPLATE_PATH + tmpl + ": " + err.Error()) | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										25
									
								
								frontend/template_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/template_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| 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) | ||||
| } | ||||
							
								
								
									
										151
									
								
								frontend/test_data/bgpmap_case1.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								frontend/test_data/bgpmap_case1.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| 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) | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user