Compare commits

...

3 Commits

Author SHA1 Message Date
600bafe08d
Add utility functions for filtering results and rename templates 2021-01-12 10:15:54 +00:00
66e63c66a1
Fix bindata build step and parameterize docker build 2021-01-12 10:06:06 +00:00
166234fa89
- Use bindata to package static file content in to the frontend binary
- Add golang templates to move HTML rendering out of the go code where possible
- Add an endpoint for serving static files
- Add URL escaping for servers and targets
2021-01-11 14:48:57 +00:00
27 changed files with 497 additions and 402 deletions

5
.gitignore vendored
View File

@ -16,4 +16,7 @@
.DS_Store
frontend/frontend
proxy/proxy
proxy/proxy
# don't include generated bindata file
frontend/bindata.go

View File

@ -31,8 +31,9 @@ script:
- |
# Build image
docker build \
--build-arg IMAGE_ARCH=$IMAGE_ARCH \
-t $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH \
-f $PROGRAM/Dockerfile.$IMAGE_ARCH \
-f $PROGRAM/Dockerfile \
$PROGRAM
# Tag image :{arch} and :{arch}-build{build number}

19
frontend/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM golang:buster AS step_0
#
# IMAGE_ARCH is the binary format of the final output
# BUILD_ARCH is the binaary format of the build host
#
ARG IMAGE_ARCH=amd64
ARG BUILD_ARCH=amd64
#
ENV CGO_ENABLED=0 GOOS=linux GOARCH=$IMAGE_ARCH GO111MODULE=on
WORKDIR /root
COPY . .
# go-bindata is run on the build host as part of the go generate step
RUN GOARCH=$BUILD_ARCH go get -u github.com/kevinburke/go-bindata/...
RUN go generate
RUN go build -ldflags "-w -s" -o /frontend
FROM scratch AS step_1
COPY --from=step_0 /frontend /
ENTRYPOINT ["/frontend"]

View File

@ -1,9 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /frontend
FROM scratch AS step_1
COPY --from=step_0 /frontend /
ENTRYPOINT ["/frontend"]

View File

@ -1,9 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=arm GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /frontend
FROM scratch AS step_1
COPY --from=step_0 /frontend /
ENTRYPOINT ["/frontend"]

View File

@ -1,9 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /frontend
FROM scratch AS step_1
COPY --from=step_0 /frontend /
ENTRYPOINT ["/frontend"]

View File

@ -1,9 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=386 GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /frontend
FROM scratch AS step_1
COPY --from=step_0 /frontend /
ENTRYPOINT ["/frontend"]

View File

@ -1,9 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /frontend
FROM scratch AS step_1
COPY --from=step_0 /frontend /
ENTRYPOINT ["/frontend"]

View File

@ -1,9 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=s390x GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /frontend
FROM scratch AS step_1
COPY --from=step_0 /frontend /
ENTRYPOINT ["/frontend"]

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -0,0 +1,14 @@
<h2>BGPmap: {{ html .Target }}</h2>
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script>
<script>
var viz = new Viz();
viz.renderSVGElement(`{{ .Result }}`)
.then(element => {
document.body.appendChild(element);
})
.catch(error => {
document.body.innerHTML = "<pre>"+error+"</pre>"
});
</script>

View File

@ -0,0 +1,2 @@
<h2>{{ html .ServerName }}: {{ html .Target }}</h2>
{{ .Result }}

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="renderer" content="webkit">
<title>{{ .Title }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous">
<meta name="robots" content="noindex, nofollow">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">{{ .Brand }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
{{ $option := .URLOption }}
{{ $server := .URLServer }}
{{ $target := .URLCommand }}
{{ if .IsWhois }}
{{ $option = "summary" }}
{{ $server = .AllServersURL }}
{{ $target = "" }}
{{ end }}
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}"
href="/{{ $option }}/{{ .AllServersURL }}/{{ $target }}"> All Servers </a>
</li>
{{ range $k, $v := .Servers }}
<li class="nav-item">
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
href="/{{ $option }}/{{ $v }}/{{ $target }}">{{ $v }}</a>
</li>
{{ end }}
</ul>
{{ if .IsWhois }}
{{ $target = .WhoisTarget }}
{{ end }}
<form class="form-inline" action="/redir" method="GET">
<div class="input-group">
<select name="action" class="form-control">
{{ range $k, $v := .Options }}
<option value="{{ $k }}"{{ if eq $k $.URLOption }} selected{{end}}>{{ $v }}</option>
{{ end }}
</select>
<input name="server" class="d-none" value="{{ $server }}">
<input name="target" class="form-control" placeholder="Target" aria-label="Target" value="{{ $target }}">
<div class="input-group-append">
<button class="btn btn-outline-success" type="submit">&raquo;</button>
</div>
</div>
</form>
</div>
</nav>
<div class="container">
{{ .Content }}
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1,26 @@
{{ $ServerName := urlquery .ServerName }}
<table class="table table-striped table-bordered table-sm">
<thead>
{{ range .Header }}
<th scope="col">{{ html . }}</th>
{{ end }}
</thead>
<tbody>
{{ range .Rows }}
<tr class="table-{{ .MappedState }}">
<td><a href="/detail/{{ $ServerName }}/{{ urlquery .Name }}">{{ html .Name }}</a></td>
<td>{{ .Proto }}</td>
<td>{{ .Table }}</td>
<td>{{ .State }}</td>
<td>{{ .Since }}</td>
<td>{{ .Info }}</td>
</tr>
{{ end }}
</tbody>
</table>
<!--
{{ .Raw }}
-->

View File

@ -0,0 +1,2 @@
<h2>whois {{ html .Target }}</h2>
{{ .Result }}

View File

@ -2,4 +2,7 @@ module github.com/xddxdd/bird-lg-go/frontend
go 1.15
require github.com/gorilla/handlers v1.5.1
require (
github.com/elazarl/go-bindata-assetfs v1.0.1
github.com/gorilla/handlers v1.5.1
)

View File

@ -1,3 +1,5 @@
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=

View File

@ -7,6 +7,9 @@ import (
"strings"
)
// binary data
//go:generate go-bindata -prefix bindata -o bindata.go bindata/...
type settingType struct {
servers []string
domain string
@ -93,5 +96,6 @@ func main() {
*navBarBrandPtr,
}
ImportTemplates()
webServerStart()
}

View File

@ -1,13 +1,41 @@
package main
import (
"bytes"
"fmt"
"net/http"
"regexp"
"sort"
"strings"
)
func renderTemplate(w http.ResponseWriter, r *http.Request, title string, content string) {
// static options map
var optionsMap = map[string]string{
"summary": "show protocols",
"detail": "show protocols all",
"route": "show route for ...",
"route_all": "show route for ... all",
"route_bgpmap": "show route for ... (bgpmap)",
"route_where": "show route where net ~ [ ... ]",
"route_where_all": "show route where net ~ [ ... ] all",
"route_where_bgpmap": "show route where net ~ [ ... ] (bgpmap)",
"route_generic": "show route ...",
"generic": "show ...",
"whois": "whois ...",
"traceroute": "traceroute ...",
}
// pre-compiled regexp and constant statemap for summary rendering
var splitSummaryLine = regexp.MustCompile(`(\w+)(\s+)(\w+)(\s+)([\w-]+)(\s+)(\w+)(\s+)([0-9\-\. :]+)(.*)`)
var summaryStateMap = map[string]string{
"up": "success",
"down": "secondary",
"start": "danger",
"passive": "info",
}
// render the page template
func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, content string) {
path := r.URL.Path[1:]
split := strings.SplitN(path, "/", 3)
@ -24,36 +52,28 @@ func renderTemplate(w http.ResponseWriter, r *http.Request, title string, conten
split = strings.SplitN(path, "/", 3)
var args tmplArguments
args.Options = 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 ...",
args := TemplatePage{
Options: optionsMap,
Servers: setting.servers,
AllServersLinkActive: strings.ToLower(split[1]) == strings.ToLower(strings.Join(setting.servers, "+")),
AllServersURL: strings.Join(setting.servers, "+"),
IsWhois: isWhois,
WhoisTarget: whoisTarget,
URLOption: strings.ToLower(split[0]),
URLServer: strings.ToLower(split[1]),
URLCommand: split[2],
Title: setting.titleBrand + title,
Brand: setting.navBarBrand,
Content: content,
}
args.Servers = setting.servers
args.AllServersLinkActive = strings.ToLower(split[1]) == strings.ToLower(strings.Join(setting.servers, "+"))
args.AllServersURL = strings.Join(setting.servers, "+")
args.IsWhois = isWhois
args.WhoisTarget = whoisTarget
args.URLOption = strings.ToLower(split[0])
args.URLServer = strings.ToLower(split[1])
args.URLCommand = split[2]
tmpl := TemplateLibrary["page"]
err := tmpl.Execute(w, args)
if err != nil {
fmt.Println("Error rendering page:", err.Error())
}
args.Title = setting.titleBrand + title
args.Brand = setting.navBarBrand
args.Content = content
tmpl.Execute(w, args)
}
// Write the given text to http response, and add whois links for
@ -77,87 +97,81 @@ func smartFormatter(s string) string {
return result
}
type summaryTableArguments struct {
Headers []string
Lines [][]string
}
// Output a table for the summary page
func summaryTable(data string, serverName string) string {
var result string
// Sort the table, excluding title row
stringsSplitted := strings.Split(strings.TrimSpace(data), "\n")
if len(stringsSplitted) <= 1 {
lines := strings.Split(strings.TrimSpace(data), "\n")
if len(lines) <= 1 {
// Likely backend returned an error message
result = "<pre>" + strings.TrimSpace(data) + "</pre>"
} else {
// Draw the table head
result += `<table class="table table-striped table-bordered table-sm">`
result += `<thead>`
for _, col := range strings.Split(stringsSplitted[0], " ") {
colTrimmed := strings.TrimSpace(col)
if len(colTrimmed) == 0 {
continue
}
result += `<th scope="col">` + colTrimmed + `</th>`
}
result += `</thead><tbody>`
stringsWithoutTitle := stringsSplitted[1:]
sort.Strings(stringsWithoutTitle)
for _, line := range stringsWithoutTitle {
// Ignore empty lines
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
// Parse a total of 6 columns from bird summary
lineSplitted := regexp.MustCompile(`(\w+)(\s+)(\w+)(\s+)([\w-]+)(\s+)(\w+)(\s+)([0-9\-\. :]+)(.*)`).FindStringSubmatch(line)
if lineSplitted == nil {
continue
}
var row [6]string
if len(lineSplitted) >= 2 {
row[0] = strings.TrimSpace(lineSplitted[1])
}
if len(lineSplitted) >= 4 {
row[1] = strings.TrimSpace(lineSplitted[3])
}
if len(lineSplitted) >= 6 {
row[2] = strings.TrimSpace(lineSplitted[5])
}
if len(lineSplitted) >= 8 {
row[3] = strings.TrimSpace(lineSplitted[7])
}
if len(lineSplitted) >= 10 {
row[4] = strings.TrimSpace(lineSplitted[9])
}
if len(lineSplitted) >= 11 {
row[5] = strings.TrimSpace(lineSplitted[10])
}
// Draw the row in red if the link isn't up
result += `<tr class="` + (map[string]string{
"up": "table-success",
"down": "table-secondary",
"start": "table-danger",
"passive": "table-info",
})[row[3]] + `">`
// Add link to detail for first column
result += `<td><a href="/detail/` + serverName + `/` + row[0] + `">` + row[0] + `</a></td>`
// Draw the other cells
for i := 1; i < 6; i++ {
result += "<td>" + row[i] + "</td>"
}
result += "</tr>"
}
result += "</tbody></table>"
result += "<!--" + data + "-->"
return "<pre>" + strings.TrimSpace(data) + "</pre>"
}
return result
args := TemplateSummary{
ServerName: serverName,
Raw: data,
}
// extract the table header
for _, col := range strings.Split(lines[0], " ") {
colTrimmed := strings.TrimSpace(col)
if len(colTrimmed) == 0 {
continue
}
args.Header = append(args.Header, col)
}
// sort the remaining rows
rows := lines[1:]
sort.Strings(rows)
// parse each line
for _, line := range rows {
// Ignore empty lines
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
// Parse a total of 6 columns from bird summary
lineSplitted := splitSummaryLine.FindStringSubmatch(line)
if lineSplitted == nil {
continue
}
var row SummaryRowData
if len(lineSplitted) >= 2 {
row.Name = strings.TrimSpace(lineSplitted[1])
}
if len(lineSplitted) >= 4 {
row.Proto = strings.TrimSpace(lineSplitted[3])
}
if len(lineSplitted) >= 6 {
row.Table = strings.TrimSpace(lineSplitted[5])
}
if len(lineSplitted) >= 8 {
row.State = strings.TrimSpace(lineSplitted[7])
row.MappedState = summaryStateMap[row.State]
}
if len(lineSplitted) >= 10 {
row.Since = strings.TrimSpace(lineSplitted[9])
}
if len(lineSplitted) >= 11 {
row.Info = strings.TrimSpace(lineSplitted[10])
}
// add to the result
args.Rows = append(args.Rows, row)
}
// finally, render the summary template
tmpl := TemplateLibrary["summary"]
var buffer bytes.Buffer
err := tmpl.Execute(&buffer, args)
if err != nil {
fmt.Println("Error rendering summary:", err.Error())
}
return buffer.String()
}

View File

@ -1,10 +1,14 @@
package main
import (
"strings"
"text/template"
)
type tmplArguments struct {
// template argument structures
// page
type TemplatePage struct {
// Global options
Options map[string]string
Servers []string
@ -27,73 +31,90 @@ type tmplArguments struct {
Content string
}
var tmpl = template.Must(template.New("tmpl").Parse(`
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="renderer" content="webkit">
<title>{{ .Title }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous">
<meta name="robots" content="noindex, nofollow">
</head>
<body>
// summary
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">{{ .Brand }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
type SummaryRowData struct {
Name string
Proto string
Table string
State string
MappedState string
Since string
Info string
}
<div class="collapse navbar-collapse" id="navbarSupportedContent">
{{ $option := .URLOption }}
{{ $server := .URLServer }}
{{ $target := .URLCommand }}
{{ if .IsWhois }}
{{ $option = "summary" }}
{{ $server = .AllServersURL }}
{{ $target = "" }}
{{ end }}
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}"
href="/{{ $option }}/{{ .AllServersURL }}/{{ $target }}"> All Servers </a>
</li>
{{ range $k, $v := .Servers }}
<li class="nav-item">
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
href="/{{ $option }}/{{ $v }}/{{ $target }}">{{ $v }}</a>
</li>
{{ end }}
</ul>
{{ if .IsWhois }}
{{ $target = .WhoisTarget }}
{{ end }}
<form class="form-inline" action="/redir" method="GET">
<div class="input-group">
<select name="action" class="form-control">
{{ range $k, $v := .Options }}
<option value="{{ $k }}"{{ if eq $k $.URLOption }} selected{{end}}>{{ $v }}</option>
{{ end }}
</select>
<input name="server" class="d-none" value="{{ $server }}">
<input name="target" class="form-control" placeholder="Target" aria-label="Target" value="{{ $target }}">
<div class="input-group-append">
<button class="btn btn-outline-success" type="submit">&raquo;</button>
</div>
</div>
</form>
</div>
</nav>
// utility functions to allow filtering of results in the template
<div class="container">
{{ .Content }}
</div>
func (r SummaryRowData) NameHasPrefix(prefix string) bool {
return strings.HasPrefix(r.Name, prefix)
}
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
</body>
</html>
`))
func (r SummaryRowData) NameContains(prefix string) bool {
return strings.Contains(r.Name, prefix)
}
type TemplateSummary struct {
ServerName string
Raw string
Header []string
Rows []SummaryRowData
}
// whois
type TemplateWhois struct {
Target string
Result string
}
// bgpmap
type TemplateBGPmap struct {
Servers []string
Target string
Result string
}
// bird
type TemplateBird struct {
ServerName string
Target string
Result string
}
// global variable to hold the templates
var TemplateLibrary map[string]*template.Template
// list of required templates
var requiredTemplates = [...]string{
"page",
"summary",
"whois",
"bgpmap",
"bird",
}
// import templates from bindata
func ImportTemplates() {
// create a new (blank) initial template
TemplateLibrary = make(map[string]*template.Template)
// for each template that is needed
for _, tmpl := range requiredTemplates {
// extract the template definition from the bindata
def := MustAssetString("templates/" + tmpl + ".tpl")
// and add it to the template library
template, err := template.New(tmpl).Parse(def)
if err != nil {
panic("Unable to parse template (templates/" + tmpl + ": " + err.Error())
}
// store in the library
TemplateLibrary[tmpl] = template
}
}

View File

@ -1,47 +1,82 @@
package main
import (
"bytes"
"fmt"
"html"
"net/http"
"net/url"
"os"
"strings"
"github.com/elazarl/go-bindata-assetfs"
"github.com/gorilla/handlers"
)
func webHandlerWhois(w http.ResponseWriter, r *http.Request) {
var target string = r.URL.Path[len("/whois/"):]
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",
}
renderTemplate(
// serve up a generic error
func serverError(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 Internal Server Error"))
}
// WHOIS pages
func webHandlerWhois(w http.ResponseWriter, r *http.Request) {
target, err := url.PathUnescape(r.URL.Path[len("/whois/"):])
if err != nil {
serverError(w, r)
return
}
// render the whois template
args := TemplateWhois{
Target: target,
Result: smartFormatter(whois(target)),
}
tmpl := TemplateLibrary["whois"]
var buffer bytes.Buffer
err = tmpl.Execute(&buffer, args)
if err != nil {
fmt.Println("Error rendering whois template:", err.Error())
}
renderPageTemplate(
w, r,
" - whois "+html.EscapeString(target),
"<h2>whois "+html.EscapeString(target)+"</h2>"+smartFormatter(whois(target)),
buffer.String(),
)
}
// serve up results from bird
func webBackendCommunicator(endpoint string, command string) func(w http.ResponseWriter, r *http.Request) {
backendCommandPrimitive, commandPresent := (map[string]string{
"summary": "show protocols",
"detail": "show protocols all %s",
"route": "show route for %s",
"route_all": "show route for %s all",
"route_where": "show route where net ~ [ %s ]",
"route_where_all": "show route where net ~ [ %s ] all",
"route_generic": "show route %s",
"generic": "show %s",
"traceroute": "%s",
})[command]
backendCommandPrimitive, commandPresent := primitiveMap[command]
if !commandPresent {
panic("invalid command: " + command)
}
return func(w http.ResponseWriter, r *http.Request) {
split := strings.SplitN(r.URL.Path[1:], "/", 4)
split := strings.SplitN(r.URL.Path[1:], "/", 3)
var urlCommands string
if len(split) >= 3 {
urlCommands = split[2]
tmp, err := url.PathUnescape(split[2])
if err != nil {
serverError(w, r)
return
}
urlCommands = tmp
}
var backendCommand string
@ -52,26 +87,50 @@ func webBackendCommunicator(endpoint string, command string) func(w http.Respons
}
backendCommand = strings.TrimSpace(backendCommand)
var servers []string = strings.Split(split[1], "+")
escapedServers, err := url.PathUnescape(split[1])
if err != nil {
serverError(w, r)
return
}
servers := strings.Split(escapedServers, "+")
var responses []string = batchRequest(servers, endpoint, backendCommand)
var result string
var content string
for i, response := range responses {
result += "<h2>" + html.EscapeString(servers[i]) + ": " + html.EscapeString(backendCommand) + "</h2>"
var result string
if (endpoint == "bird") && backendCommand == "show protocols" && len(response) > 4 && strings.ToLower(response[0:4]) == "name" {
result += summaryTable(response, servers[i])
result = summaryTable(response, servers[i])
} else {
result += smartFormatter(response)
result = smartFormatter(response)
}
// render the bird result template
args := TemplateBird{
ServerName: servers[i],
Target: backendCommand,
Result: result,
}
tmpl := TemplateLibrary["bird"]
var buffer bytes.Buffer
err := tmpl.Execute(&buffer, args)
if err != nil {
fmt.Println("Error rendering bird template:", err.Error())
}
content += buffer.String()
}
renderTemplate(
renderPageTemplate(
w, r,
" - "+html.EscapeString(endpoint+" "+backendCommand),
result,
content,
)
}
}
// bgpmap result
func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWriter, r *http.Request) {
backendCommandPrimitive, commandPresent := (map[string]string{
"route_bgpmap": "show route for %s all",
@ -95,51 +154,70 @@ func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWrite
var servers []string = strings.Split(split[1], "+")
var responses []string = batchRequest(servers, endpoint, backendCommand)
renderTemplate(
// render the bgpmap result template
args := TemplateBGPmap{
Servers: servers,
Target: backendCommand,
Result: birdRouteToGraphviz(servers, responses, urlCommands),
}
tmpl := TemplateLibrary["bgpmap"]
var buffer bytes.Buffer
err := tmpl.Execute(&buffer, args)
if err != nil {
fmt.Println("Error rendering bgpmap template:", err.Error())
}
renderPageTemplate(
w, r,
" - "+html.EscapeString(endpoint+" "+backendCommand),
`
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script>
<script>
var viz = new Viz();
viz.renderSVGElement(`+"`"+birdRouteToGraphviz(servers, responses, urlCommands)+"`"+`)
.then(element => {
document.body.appendChild(element);
})
.catch(error => {
document.body.innerHTML = "<pre>"+error+"</pre>"
});
</script>`,
buffer.String(),
)
}
}
// redirect from the form input to a path style query
func webHandlerNavbarFormRedirect(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
if query.Get("action") == "whois" {
http.Redirect(w, r, "/"+query.Get("action")+"/"+query.Get("target"), 302)
} else if query.Get("action") == "summary" {
http.Redirect(w, r, "/"+query.Get("action")+"/"+query.Get("server")+"/", 302)
} else {
http.Redirect(w, r, "/"+query.Get("action")+"/"+query.Get("server")+"/"+query.Get("target"), 302)
action := query.Get("action")
switch action {
case "whois":
target := url.PathEscape(query.Get("target"))
http.Redirect(w, r, "/"+action+"/"+target, 302)
case "summary":
server := url.PathEscape(query.Get("server"))
http.Redirect(w, r, "/"+action+"/"+server+"/", 302)
default:
server := url.PathEscape(query.Get("server"))
target := url.PathEscape(query.Get("target"))
http.Redirect(w, r, "/"+action+"/"+server+"/"+target, 302)
}
}
func webHandlerRobotsTxt(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("User-agent: *\nDisallow: /\n"))
}
func webHandler404(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 not found\n"))
}
// set up routing paths and start webserver
func webServerStart() {
// Start HTTP server
// redirect main page to all server summary
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/summary/"+strings.Join(setting.servers, "+"), 302)
})
// serve static pages using the AssetFS and bindata
fs := http.FileServer(&assetfs.AssetFS{
Asset: Asset,
AssetDir: AssetDir,
AssetInfo: AssetInfo,
Prefix: "",
})
http.Handle("/static/", fs)
http.Handle("/robots.txt", fs)
http.Handle("/favicon.ico", fs)
// backend routes
http.HandleFunc("/summary/", webBackendCommunicator("bird", "summary"))
http.HandleFunc("/detail/", webBackendCommunicator("bird", "detail"))
http.HandleFunc("/route/", webBackendCommunicator("bird", "route"))
@ -154,7 +232,7 @@ func webServerStart() {
http.HandleFunc("/whois/", webHandlerWhois)
http.HandleFunc("/redir", webHandlerNavbarFormRedirect)
http.HandleFunc("/telegram/", webHandlerTelegramBot)
http.HandleFunc("/robots.txt", webHandlerRobotsTxt)
http.HandleFunc("/favicon.ico", webHandler404)
// Start HTTP server
http.ListenAndServe(setting.listen, handlers.LoggingHandler(os.Stdout, http.DefaultServeMux))
}

View File

@ -1,8 +1,13 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on
#
# IMAGE_ARCH is the binary format of the final output
#
ARG IMAGE_ARCH=amd64
#
ENV CGO_ENABLED=0 GOOS=linux GOARCH=$IMAGE_ARCH GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /proxy
RUN go build -ldflags "-w -s" -o /proxy
FROM amd64/debian AS step_1
ENV TARGET_ARCH=x86_64

View File

@ -1,23 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=arm GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /proxy
FROM arm32v7/debian AS step_1
ENV TARGET_ARCH=arm
WORKDIR /root
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y \
build-essential musl-dev musl-tools tar wget git
RUN git clone https://github.com/sabotage-linux/kernel-headers.git
RUN wget https://sourceforge.net/projects/traceroute/files/traceroute/traceroute-2.1.0/traceroute-2.1.0.tar.gz/download \
-O traceroute-2.1.0.tar.gz
RUN tar xvf traceroute-2.1.0.tar.gz \
&& cd traceroute-2.1.0 \
&& make -j4 CC=musl-gcc CFLAGS="-I/root/kernel-headers/${TARGET_ARCH}/include" LDFLAGS="-static"
FROM scratch AS step_2
ENV PATH=/
COPY --from=step_0 /proxy /
COPY --from=step_1 /root/traceroute-2.1.0/traceroute/traceroute /
ENTRYPOINT ["/proxy"]

View File

@ -1,23 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /proxy
FROM arm64v8/debian AS step_1
ENV TARGET_ARCH=arm64
WORKDIR /root
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y \
build-essential musl-dev musl-tools tar wget git
RUN git clone https://github.com/sabotage-linux/kernel-headers.git
RUN wget https://sourceforge.net/projects/traceroute/files/traceroute/traceroute-2.1.0/traceroute-2.1.0.tar.gz/download \
-O traceroute-2.1.0.tar.gz
RUN tar xvf traceroute-2.1.0.tar.gz \
&& cd traceroute-2.1.0 \
&& make -j4 CC=musl-gcc CFLAGS="-I/root/kernel-headers/${TARGET_ARCH}/include" LDFLAGS="-static"
FROM scratch AS step_2
ENV PATH=/
COPY --from=step_0 /proxy /
COPY --from=step_1 /root/traceroute-2.1.0/traceroute/traceroute /
ENTRYPOINT ["/proxy"]

View File

@ -1,23 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=386 GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /proxy
FROM i386/debian AS step_1
ENV TARGET_ARCH=x86
WORKDIR /root
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y \
build-essential musl-dev musl-tools tar wget git
RUN git clone https://github.com/sabotage-linux/kernel-headers.git
RUN wget https://sourceforge.net/projects/traceroute/files/traceroute/traceroute-2.1.0/traceroute-2.1.0.tar.gz/download \
-O traceroute-2.1.0.tar.gz
RUN tar xvf traceroute-2.1.0.tar.gz \
&& cd traceroute-2.1.0 \
&& make -j4 CC=musl-gcc CFLAGS="-I/root/kernel-headers/${TARGET_ARCH}/include" LDFLAGS="-static"
FROM scratch AS step_2
ENV PATH=/
COPY --from=step_0 /proxy /
COPY --from=step_1 /root/traceroute-2.1.0/traceroute/traceroute /
ENTRYPOINT ["/proxy"]

View File

@ -1,23 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /proxy
FROM ppc64le/debian AS step_1
ENV TARGET_ARCH=ppc64le
WORKDIR /root
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y \
build-essential musl-dev musl-tools tar wget git
RUN git clone https://github.com/sabotage-linux/kernel-headers.git
RUN wget https://sourceforge.net/projects/traceroute/files/traceroute/traceroute-2.1.0/traceroute-2.1.0.tar.gz/download \
-O traceroute-2.1.0.tar.gz
RUN tar xvf traceroute-2.1.0.tar.gz \
&& cd traceroute-2.1.0 \
&& make -j4 CC=musl-gcc CFLAGS="-I/root/kernel-headers/${TARGET_ARCH}/include" LDFLAGS="-static"
FROM scratch AS step_2
ENV PATH=/
COPY --from=step_0 /proxy /
COPY --from=step_1 /root/traceroute-2.1.0/traceroute/traceroute /
ENTRYPOINT ["/proxy"]

View File

@ -1,23 +0,0 @@
FROM golang:buster AS step_0
ENV CGO_ENABLED=0 GOOS=linux GOARCH=s390x GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -o /proxy
FROM s390x/debian AS step_1
ENV TARGET_ARCH=s390
WORKDIR /root
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y \
build-essential musl-dev musl-tools tar wget git
RUN git clone https://github.com/sabotage-linux/kernel-headers.git
RUN wget https://sourceforge.net/projects/traceroute/files/traceroute/traceroute-2.1.0/traceroute-2.1.0.tar.gz/download \
-O traceroute-2.1.0.tar.gz
RUN tar xvf traceroute-2.1.0.tar.gz \
&& cd traceroute-2.1.0 \
&& make -j4 CC=musl-gcc CFLAGS="-I/root/kernel-headers/${TARGET_ARCH}/include" LDFLAGS="-static"
FROM scratch AS step_2
ENV PATH=/
COPY --from=step_0 /proxy /
COPY --from=step_1 /root/traceroute-2.1.0/traceroute/traceroute /
ENTRYPOINT ["/proxy"]