You've already forked bird-lg-go
							
							
				mirror of
				https://github.com/xddxdd/bird-lg-go
				synced 2025-11-02 13:02:29 +01:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b237185ef7 | ||
| 
						 | 
					e949646790 | ||
| 
						 | 
					bb479d22ae | ||
| 
						 | 
					d40f41b4d5 | ||
| 
						 | 
					cdc34704b5 | ||
| 
						 | 
					db58bd3354 | ||
| 
						 | 
					a0246ccee2 | ||
| 
						 | 
					ccd14af0c8 | ||
| 
						 | 
					594ca80f50 | ||
| 
						 | 
					5625058e71 | ||
| 
						 | 
					7efa3237a9 | ||
| 
						 | 
					7b0c8c0556 | ||
| 
						 | 
					ffd9165062 | ||
| 
						 | 
					24fd5203e8 | ||
| 
						 | 
					49a05767c1 | ||
| 
						 | 
					e7010f75f8 | ||
| 
						 | 
					dba2af7634 | ||
| 
						 | 
					049775319b | ||
| 
						 | 
					47c66b125c | ||
| 
						 | 
					9e17b116f1 | ||
| 
						 | 
					335ad40634 | ||
| 
						 | 
					6ec0f2e7a6 | ||
| 
						 | 
					4b73cf0fcb | ||
| 
						 | 
					3b1d001543 | ||
| 
						 | 
					675cb26ed1 | ||
| 
						 | 
					556d3e50d3 | ||
| 
						 | 
					06796f546e | ||
| 
						 | 
					d029d6684c | ||
| 
						 | 
					5ce0f55f35 | 
@@ -4,14 +4,6 @@ workflows:
 | 
			
		||||
  docker:
 | 
			
		||||
    jobs:
 | 
			
		||||
      - build
 | 
			
		||||
      - docker-frontend:
 | 
			
		||||
          context:
 | 
			
		||||
            - docker
 | 
			
		||||
          requires:
 | 
			
		||||
            - build
 | 
			
		||||
          filters:
 | 
			
		||||
            branches:
 | 
			
		||||
              ignore: master
 | 
			
		||||
      - docker-frontend-deploy:
 | 
			
		||||
          context:
 | 
			
		||||
            - docker
 | 
			
		||||
@@ -20,14 +12,6 @@ workflows:
 | 
			
		||||
          filters:
 | 
			
		||||
            branches:
 | 
			
		||||
              only: master
 | 
			
		||||
      - docker-proxy:
 | 
			
		||||
          context:
 | 
			
		||||
            - docker
 | 
			
		||||
          requires:
 | 
			
		||||
            - build
 | 
			
		||||
          filters:
 | 
			
		||||
            branches:
 | 
			
		||||
              ignore: master
 | 
			
		||||
      - docker-proxy-deploy:
 | 
			
		||||
          context:
 | 
			
		||||
            - docker
 | 
			
		||||
@@ -59,43 +43,6 @@ jobs:
 | 
			
		||||
            go get -v -t -d ./...
 | 
			
		||||
            go test -v ./...
 | 
			
		||||
 | 
			
		||||
  docker-frontend:
 | 
			
		||||
    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 \
 | 
			
		||||
              frontend
 | 
			
		||||
 | 
			
		||||
  docker-frontend-deploy:
 | 
			
		||||
    machine:
 | 
			
		||||
      image: ubuntu-2004:202111-02
 | 
			
		||||
@@ -138,43 +85,6 @@ jobs:
 | 
			
		||||
              --progress plain \
 | 
			
		||||
              --push frontend
 | 
			
		||||
 | 
			
		||||
  docker-proxy:
 | 
			
		||||
    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 \
 | 
			
		||||
              --progress plain \
 | 
			
		||||
              proxy
 | 
			
		||||
 | 
			
		||||
  docker-proxy-deploy:
 | 
			
		||||
    machine:
 | 
			
		||||
      image: ubuntu-2004:202111-02
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							@@ -19,14 +19,14 @@ jobs:
 | 
			
		||||
            goos: windows
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v3
 | 
			
		||||
    - uses: wangyoucao577/go-release-action@v1.30
 | 
			
		||||
    - uses: wangyoucao577/go-release-action@v1.34
 | 
			
		||||
      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
 | 
			
		||||
    - uses: wangyoucao577/go-release-action@v1.34
 | 
			
		||||
      with:
 | 
			
		||||
        github_token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
        goos: ${{ matrix.goos }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								README.md
									
									
									
									
									
								
							@@ -58,15 +58,13 @@ 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 |
 | 
			
		||||
| 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") |
 | 
			
		||||
| 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") |
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -122,16 +122,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 "") |
 | 
			
		||||
| 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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								docs/API.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								docs/API.md
									
									
									
									
									
								
							@@ -153,9 +153,11 @@ Request:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "servers": [],
 | 
			
		||||
    "type": "server_list",
 | 
			
		||||
    "args": ""
 | 
			
		||||
    "servers": [
 | 
			
		||||
        "alpha"
 | 
			
		||||
    ],
 | 
			
		||||
    "type": "bird",
 | 
			
		||||
    "args": "show status"
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@@ -179,11 +181,9 @@ Request:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "servers": [
 | 
			
		||||
        "alpha"
 | 
			
		||||
    ],
 | 
			
		||||
    "type": "bird",
 | 
			
		||||
    "args": "show status"
 | 
			
		||||
    "servers": [],
 | 
			
		||||
    "type": "server_list",
 | 
			
		||||
    "args": ""
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -113,7 +113,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)
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										207
									
								
								frontend/api_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								frontend/api_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,207 @@
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								frontend/assets/static/sortTable.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								frontend/assets/static/sortTable.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
// 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);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
@@ -60,7 +60,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>
 | 
			
		||||
@@ -76,6 +76,7 @@
 | 
			
		||||
 | 
			
		||||
<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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{{ $ServerName := urlquery .ServerName }}
 | 
			
		||||
 | 
			
		||||
<table class="table table-striped table-bordered table-sm">
 | 
			
		||||
<table class="table table-striped table-bordered table-sm sortable">
 | 
			
		||||
  <thead>
 | 
			
		||||
{{ range .Header }}
 | 
			
		||||
    <th scope="col">{{ html . }}</th>
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										173
									
								
								frontend/bgpmap_graph.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								frontend/bgpmap_graph.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
			
		||||
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,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) {
 | 
			
		||||
@@ -85,3 +37,48 @@ func TestBirdRouteToGraphvizXSS(t *testing.T) {
 | 
			
		||||
		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,14 +1,29 @@
 | 
			
		||||
module github.com/xddxdd/bird-lg-go/frontend
 | 
			
		||||
 | 
			
		||||
go 1.16
 | 
			
		||||
go 1.17
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 | 
			
		||||
	github.com/gorilla/handlers v1.5.1
 | 
			
		||||
	github.com/jarcoal/httpmock v1.3.0
 | 
			
		||||
	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/gorilla/handlers v1.5.1
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.0.2 // indirect
 | 
			
		||||
	github.com/spf13/pflag v1.0.5
 | 
			
		||||
	github.com/spf13/viper v1.12.0
 | 
			
		||||
	github.com/subosito/gotenv v1.4.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d // indirect
 | 
			
		||||
	gopkg.in/ini.v1 v1.66.6 // 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
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1012
									
								
								frontend/go.sum
									
									
									
									
									
								
							
							
						
						
									
										1012
									
								
								frontend/go.sum
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -56,8 +56,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")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -13,18 +13,26 @@ import (
 | 
			
		||||
 | 
			
		||||
// static options map
 | 
			
		||||
var optionsMap = map[string]string{
 | 
			
		||||
	"summary":            "show protocols",
 | 
			
		||||
	"detail":             "show protocols all",
 | 
			
		||||
	"route":              "show route for ...",
 | 
			
		||||
	"route_all":          "show route for ... all",
 | 
			
		||||
	"route_bgpmap":       "show route for ... (bgpmap)",
 | 
			
		||||
	"route_where":        "show route where net ~ [ ... ]",
 | 
			
		||||
	"route_where_all":    "show route where net ~ [ ... ] all",
 | 
			
		||||
	"route_where_bgpmap": "show route where net ~ [ ... ] (bgpmap)",
 | 
			
		||||
	"route_generic":      "show route ...",
 | 
			
		||||
	"generic":            "show ...",
 | 
			
		||||
	"whois":              "whois ...",
 | 
			
		||||
	"traceroute":         "traceroute ...",
 | 
			
		||||
	"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 ...",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// pre-compiled regexp and constant statemap for summary rendering
 | 
			
		||||
 
 | 
			
		||||
@@ -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"}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,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("-", "_", ".", "_"))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
@@ -17,15 +17,26 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var primitiveMap = map[string]string{
 | 
			
		||||
	"summary":         "show protocols",
 | 
			
		||||
	"detail":          "show protocols all %s",
 | 
			
		||||
	"route":           "show route for %s",
 | 
			
		||||
	"route_all":       "show route for %s all",
 | 
			
		||||
	"route_where":     "show route where net ~ [ %s ]",
 | 
			
		||||
	"route_where_all": "show route where net ~ [ %s ] all",
 | 
			
		||||
	"route_generic":   "show route %s",
 | 
			
		||||
	"generic":         "show %s",
 | 
			
		||||
	"traceroute":      "%s",
 | 
			
		||||
	"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",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// serve up a generic error
 | 
			
		||||
@@ -204,6 +215,16 @@ func webServerStart(l net.Listener) {
 | 
			
		||||
	// 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"))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										89
									
								
								frontend/webserver_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								frontend/webserver_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
@@ -3,7 +3,11 @@ package main
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"net"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/shlex"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Send a whois request
 | 
			
		||||
@@ -12,18 +16,43 @@ func whois(s string) string {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conn, err := net.DialTimeout("tcp", setting.whoisServer+":43", 5*time.Second)
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	conn.Write([]byte(s + "\r\n"))
 | 
			
		||||
		cmd := exec.Command(args[0], args[1:]...)
 | 
			
		||||
		output, err := cmd.CombinedOutput()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err.Error()
 | 
			
		||||
		}
 | 
			
		||||
		if len(output) > 65535 {
 | 
			
		||||
			output = output[:65535]
 | 
			
		||||
		}
 | 
			
		||||
		return string(output)
 | 
			
		||||
	} else {
 | 
			
		||||
		buf := make([]byte, 65536)
 | 
			
		||||
 | 
			
		||||
	buf := make([]byte, 65536)
 | 
			
		||||
	n, err := io.ReadFull(conn, buf)
 | 
			
		||||
	if err != nil && err != io.ErrUnexpectedEOF {
 | 
			
		||||
		return err.Error()
 | 
			
		||||
		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()
 | 
			
		||||
		}
 | 
			
		||||
		return string(buf[:n])
 | 
			
		||||
	}
 | 
			
		||||
	return string(buf[:n])
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user