Initial commit

This commit is contained in:
Lan Tian 2019-01-08 01:32:30 +08:00
commit a9a07d4de4
No known key found for this signature in database
GPG Key ID: 27F31700E751EC22
6 changed files with 650 additions and 0 deletions

51
frontend/lgproxy.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"net/http"
"net/url"
"io/ioutil"
"strconv"
)
type channelData struct {
id int
data string
}
func batchRequest(servers []string, endpoint string, command string) []string {
var ch chan channelData = make(chan channelData)
var response_array []string = make([]string, len(servers))
for i, server := range servers {
var isValidServer bool = false
for _, validServer := range settingServers {
if validServer == server {
isValidServer = true
break
}
}
if !isValidServer {
go func (i int) {
ch <- channelData{i, "request failed: invalid server\n"}
} (i)
} else {
url := "http://" + server + "." + settingServersDomain + ":" + strconv.Itoa(settingServersPort) + "/" + url.PathEscape(endpoint) + "?q=" + url.QueryEscape(command)
go func (url string, i int){
response, err := http.Get(url)
if err != nil {
ch <- channelData{i, "request failed: " + err.Error() + "\n"}
return
}
text, _ := ioutil.ReadAll(response.Body)
ch <- channelData{i, string(text)}
} (url, i)
}
}
for range servers {
var output channelData = <-ch
response_array[output.id] = output.data
}
return response_array
}

25
frontend/main.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"flag"
"strings"
)
var settingServers []string
var settingServersDomain string
var settingServersPort int
var settingWhoisServer string
func main() {
serversPtr := flag.String("servers", "", "server name prefixes, separated by comma")
domainPtr := flag.String("domain", "", "server name domain suffixes")
portPtr := flag.Int("port", 8000, "port bird-lgproxy is running on")
whoisPtr := flag.String("whois", "whois.verisign-grs.com", "whois server for queries")
flag.Parse()
settingServers = strings.Split(*serversPtr, ",")
settingServersDomain = *domainPtr
settingServersPort = *portPtr
settingWhoisServer = *whoisPtr
webServerStart()
}

243
frontend/template.go Normal file
View File

@ -0,0 +1,243 @@
package main
import (
"net"
"net/http"
"strings"
"strconv"
)
func templateHeader(w http.ResponseWriter, r *http.Request, title string) {
path := r.URL.Path
if len(strings.Split(r.URL.Path, "/")) < 4 {
path = "/ipv4/summary/" + strings.Join(settingServers[:], "+") + "/"
}
split := strings.Split(path, "/")
split[1] = "ipv4"
ipv4_url := strings.Join(split, "/")
split = strings.Split(path, "/")
split[1] = "ipv6"
ipv6_url := strings.Join(split, "/")
split = strings.Split(path, "/")
split[3] = strings.Join(settingServers[:], "+")
all_url := strings.Join(split, "/")
split = strings.Split(path, "/")
var serverAllActive string
if split[3] == strings.Join(settingServers[:], "+") {
serverAllActive = " active"
}
var serverNavigation string = `
<li class="nav-item">
<a class="nav-link" href="` + ipv4_url + `"> IPv4 </a>
</li>
<li class="nav-item">
<a class="nav-link" href="` + ipv6_url + `"> IPv6 </a>
</li>
<span class="navbar-text">|</span>
<li class="nav-item">
<a class="nav-link` + serverAllActive + `" href="` + all_url + `"> All Servers </a>
</li>
`
for _, server := range settingServers {
split = strings.Split(path, "/")
var serverActive string
if split[3] == server {
serverActive = " active"
}
split[3] = server
server_url := strings.Join(split, "/")
serverNavigation += `
<li class="nav-item">
<a class="nav-link` + serverActive + `" href="` + server_url + `">` + server + `</a>
</li>
`
}
var options string
split = strings.Split(path, "/")
if split[2] == "summary" {
options += `<option value="summary" selected>show protocol</option>`
} else {
options += `<option value="summary">show protocol</option>`
}
if split[2] == "route" {
options += `<option value="route" selected>show route for ...</option>`
} else {
options += `<option value="route">show route for ...</option>`
}
if split[2] == "route_all" {
options += `<option value="route_all" selected>show route for ... all</option>`
} else {
options += `<option value="route_all">show route for ... all</option>`
}
if split[2] == "whois" {
options += `<option value="whois" selected>whois ...</option>`
} else {
options += `<option value="whois">whois ...</option>`
}
var target string
if len(split) >= 5 {
target = split[4]
}
w.Write([]byte(`
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN" lang="zh-CN" class="no-js">
<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,shrink-to-fit=no"/>
<meta name="renderer" content="webkit"/>
<title>` + title + `</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">Bird-lg Go</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">
<ul class="navbar-nav mr-auto">` + serverNavigation + `</ul>
<form class="form-inline" action="/redir" method="GET">
<div class="input-group">
<select name="action" class="form-control">` + options + `</select>
<input name="proto" class="d-none" value="` + split[1] + `">
<input name="server" class="d-none" value="` + split[3] + `">
<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">
`))
}
func templateFooter(w http.ResponseWriter) {
w.Write([]byte(`
</div>
<!--<script data-no-instant src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js"></script>
<script data-no-instant src="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/js/bootstrap.bundle.min.js"></script>-->
</body>
</html>
`))
}
func isIP(s string) bool {
return nil != net.ParseIP(s)
}
func isNumber(s string) bool {
_, err := strconv.Atoi(s)
return nil == err
}
func smartWriter(w http.ResponseWriter, s string) {
w.Write([]byte("<pre>"))
for _, line := range strings.Split(s, "\n") {
var tabPending bool = false
var isFirstWord bool = true
var isASes bool = false
for _, word := range strings.Split(line, " ") {
if len(word) == 0 {
tabPending = true
} else {
if isFirstWord {
isFirstWord = false
} else if tabPending {
w.Write([]byte("\t"))
tabPending = false
} else {
w.Write([]byte(" "))
}
if isIP(word) {
w.Write([]byte("<a href=\"/whois/" + word + "\">" + word + "</a>"))
} else if len(strings.Split(word, "%")) == 2 && isIP(strings.Split(word, "%")[0]) {
w.Write([]byte("<a href=\"/whois/" + strings.Split(word, "%")[0] + "\">" + strings.Split(word, "%")[0] + "</a>"))
w.Write([]byte("%" + strings.Split(word, "%")[1]))
} else if len(strings.Split(word, "/")) == 2 && isIP(strings.Split(word, "/")[0]) {
w.Write([]byte("<a href=\"/whois/" + strings.Split(word, "/")[0] + "\">" + strings.Split(word, "/")[0] + "</a>"))
w.Write([]byte("/" + strings.Split(word, "/")[1]))
} else if word == "AS:" || word == "\tBGP.as_path:" {
isASes = true
w.Write([]byte(word))
} else if isASes && isNumber(word) {
w.Write([]byte("<a href=\"/whois/AS" + word + "\">" + word + "</a>"))
} else {
w.Write([]byte(word))
}
}
}
w.Write([]byte("\n"))
}
w.Write([]byte("</pre>"))
}
func summaryTable(w http.ResponseWriter, isIPv6 bool, data string, serverName string) {
w.Write([]byte("<table class=\"table table-striped table-bordered table-sm\">"))
for lineId, line := range strings.Split(data, "\n") {
var tabPending bool = false
var tableCells int = 0
var row [6]string
for i, word := range strings.Split(line, " ") {
if len(word) == 0 {
tabPending = true
} else {
if i == 0 {
tabPending = true
} else if tabPending {
if tableCells < 5 {
tableCells++
} else {
row[tableCells] += " "
}
tabPending = false
} else {
row[tableCells] += " "
}
row[tableCells] += word
}
}
if len(row[0]) == 0 {
continue
}
if lineId == 0 {
w.Write([]byte("<thead>"))
for i := 0; i < 6; i++ {
w.Write([]byte("<th scope=\"col\">" + row[i] + "</th>"))
}
w.Write([]byte("</thead><tbody>"))
} else {
if row[3] == "up" {
w.Write([]byte("<tr>"))
} else if lineId != 0 {
w.Write([]byte("<tr class=\"table-danger\">"))
}
if isIPv6 {
w.Write([]byte("<td><a href=\"/ipv6/detail/" + serverName + "/" + row[0] + "\">" + row[0] + "</a></td>"))
} else {
w.Write([]byte("<td><a href=\"/ipv4/detail/" + serverName + "/" + row[0] + "\">" + row[0] + "</a></td>"))
}
for i := 1; i < 6; i++ {
w.Write([]byte("<td>" + row[i] + "</td>"))
}
w.Write([]byte("</tr>"))
}
}
w.Write([]byte("</tbody></table>"))
}

110
frontend/webserver.go Normal file
View File

@ -0,0 +1,110 @@
package main
import (
"net/http"
"strings"
"html"
)
func webDispatcherIPv4Summary(w http.ResponseWriter, r *http.Request) {
webHandler(w, r, false, r.URL.Path[len("/ipv4/summary/"):], "show protocols")
}
func webDispatcherIPv6Summary(w http.ResponseWriter, r *http.Request) {
webHandler(w, r, true, r.URL.Path[len("/ipv6/summary/"):], "show protocols")
}
func webDispatcherIPv4Detail(w http.ResponseWriter, r *http.Request) {
split := strings.Split(r.URL.Path[len("/ipv4/detail/"):], "/")
webHandler(w, r, false, split[0], "show protocols all " + split[1])
}
func webDispatcherIPv6Detail(w http.ResponseWriter, r *http.Request) {
split := strings.Split(r.URL.Path[len("/ipv6/detail/"):], "/")
webHandler(w, r, true, split[0], "show protocols all " + split[1])
}
func webDispatcherIPv4Route(w http.ResponseWriter, r *http.Request) {
split := strings.Split(r.URL.Path[len("/ipv4/route/"):], "/")
webHandler(w, r, false, split[0], "show route for " + strings.Join(split[1:], "/"))
}
func webDispatcherIPv6Route(w http.ResponseWriter, r *http.Request) {
split := strings.Split(r.URL.Path[len("/ipv6/route/"):], "/")
webHandler(w, r, true, split[0], "show route for " + strings.Join(split[1:], "/"))
}
func webDispatcherIPv4RouteAll(w http.ResponseWriter, r *http.Request) {
split := strings.Split(r.URL.Path[len("/ipv4/route_all/"):], "/")
webHandler(w, r, false, split[0], "show route for " + strings.Join(split[1:], "/") + " all")
}
func webDispatcherIPv6RouteAll(w http.ResponseWriter, r *http.Request) {
split := strings.Split(r.URL.Path[len("/ipv6/route_all/"):], "/")
webHandler(w, r, true, split[0], "show route for " + strings.Join(split[1:], "/") + " all")
}
func webDispatcherWhois(w http.ResponseWriter, r *http.Request) {
var target string = r.URL.Path[len("/whois/"):]
templateHeader(w, r, "Bird-lg Go - WHOIS " + html.EscapeString(target))
w.Write([]byte("<h2>whois " + html.EscapeString(target) + "</h2>"))
smartWriter(w, whois(target))
templateFooter(w)
}
func webHandler(w http.ResponseWriter, r *http.Request, isIPv6 bool, serverQuery string, command string) {
templateHeader(w, r, "Bird-lg Go - " + html.EscapeString(command))
var servers []string = strings.Split(serverQuery, "+")
var responses []string
if isIPv6 {
responses = batchRequest(servers, "bird6", command)
} else {
responses = batchRequest(servers, "bird", command)
}
for i, response := range responses {
w.Write([]byte("<h2>" + html.EscapeString(servers[i]) + ": " + html.EscapeString(command) + "</h2>"))
if command == "show protocols" && response[0:4] == "name" {
summaryTable(w, isIPv6, response, servers[i])
} else {
smartWriter(w, response)
}
}
templateFooter(w)
}
func defaultRedirect(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/ipv4/summary/" + strings.Join(settingServers[:], "+"), 302)
}
func navbarFormRedirect(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("proto") + "/" + query.Get("action") + "/" + query.Get("server"), 302)
} else {
http.Redirect(w, r, "/" + query.Get("proto") + "/" + query.Get("action") + "/" + query.Get("server") + "/" + query.Get("target"), 302)
}
}
func webServerStart() {
// Start HTTP server
http.HandleFunc("/", defaultRedirect)
http.HandleFunc("/ipv4/summary/", webDispatcherIPv4Summary)
http.HandleFunc("/ipv6/summary/", webDispatcherIPv6Summary)
http.HandleFunc("/ipv4/detail/", webDispatcherIPv4Detail)
http.HandleFunc("/ipv6/detail/", webDispatcherIPv6Detail)
http.HandleFunc("/ipv4/route/", webDispatcherIPv4Route)
http.HandleFunc("/ipv6/route/", webDispatcherIPv6Route)
http.HandleFunc("/ipv4/route_all/", webDispatcherIPv4RouteAll)
http.HandleFunc("/ipv6/route_all/", webDispatcherIPv6RouteAll)
http.HandleFunc("/whois/", webDispatcherWhois)
http.HandleFunc("/redir/", navbarFormRedirect)
http.ListenAndServe(":5000", nil)
}

21
frontend/whois.go Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"net"
"io/ioutil"
)
func whois(s string) string {
conn, err := net.Dial("tcp", settingWhoisServer + ":43")
if err != nil {
return err.Error()
}
defer conn.Close()
conn.Write([]byte(s + "\r\n"))
result, err := ioutil.ReadAll(conn)
if err != nil {
return err.Error()
}
return string(result)
}

200
proxy/main.go Normal file
View File

@ -0,0 +1,200 @@
package main
import (
"io"
"net"
"net/http"
"sync"
"runtime"
"os/exec"
"flag"
)
// BIRDv4 connection & mutex lock
var bird net.Conn
var birdMutex = &sync.Mutex{}
// BIRDv6 connection & mutex lock
var bird6 net.Conn
var bird6Mutex = &sync.Mutex{}
// Check if a byte is character for number
func isNumeric(b byte) bool {
return b >= byte('0') && b <= byte('9')
}
// Read a line from bird socket, removing preceding status number, output it.
// Returns if there are more lines.
func birdReadln(bird io.Reader, w io.Writer) bool {
// Read from socket byte by byte, until reaching newline character
c := make([]byte, 1024, 1024)
pos := 0
for {
if pos >= 1024 { break }
_, err := bird.Read(c[pos:pos+1])
if err != nil {
panic(err)
}
if c[pos] == byte('\n') {
break
}
pos++
}
c = c[:pos+1]
// print(string(c[:]))
// Remove preceding status number, different situations
if pos < 4 {
// Line is too short to have a status number
if w != nil {
pos = 0
for c[pos] == byte(' ') { pos++ }
w.Write(c[pos:])
}
return true
} else if isNumeric(c[0]) && isNumeric(c[1]) && isNumeric(c[2]) && isNumeric(c[3]) {
// There is a status number at beginning, remove first 5 bytes
if w != nil && pos > 6 {
pos = 5
for c[pos] == byte(' ') { pos++ }
w.Write(c[pos:])
}
return c[0] != byte('0') && c[0] != byte('8') && c[0] != byte('9')
} else {
// There is no status number, only remove preceding spaces
if w != nil {
pos = 0
for c[pos] == byte(' ') { pos++ }
w.Write(c[pos:])
}
return true
}
}
// Write a command to a bird socket
func birdWriteln(bird io.Writer, s string) {
bird.Write([]byte(s + "\n"))
}
// Default handler, returns 500 Internal Server Error
func invalidHandler(httpW http.ResponseWriter, httpR *http.Request) {
httpW.WriteHeader(http.StatusInternalServerError)
httpW.Write([]byte("Invalid Request\n"))
}
// Handles BIRDv4 queries
func birdHandler(httpW http.ResponseWriter, httpR *http.Request) {
query := string(httpR.URL.Query().Get("q"))
if query == "" {
invalidHandler(httpW, httpR)
} else {
birdMutex.Lock()
defer birdMutex.Unlock()
println(query)
birdWriteln(bird, query)
for birdReadln(bird, httpW) {}
}
}
// Handles BIRDv6 queries
func bird6Handler(httpW http.ResponseWriter, httpR *http.Request) {
query := string(httpR.URL.Query().Get("q"))
if query == "" {
invalidHandler(httpW, httpR)
} else {
bird6Mutex.Lock()
defer bird6Mutex.Unlock()
println(query)
birdWriteln(bird6, query)
for birdReadln(bird6, httpW) {}
}
}
// Wrapper of traceroute, IPv4
func tracerouteIPv4Wrapper(httpW http.ResponseWriter, httpR *http.Request) {
tracerouteRealHandler(false, httpW, httpR)
}
// Wrapper of traceroute, IPv6
func tracerouteIPv6Wrapper(httpW http.ResponseWriter, httpR *http.Request) {
tracerouteRealHandler(true, httpW, httpR)
}
// Real handler of traceroute requests
func tracerouteRealHandler(useIPv6 bool, httpW http.ResponseWriter, httpR *http.Request) {
query := string(httpR.URL.Query().Get("q"))
if query == "" {
invalidHandler(httpW, httpR)
} else {
var cmd string
var args []string
if runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" {
if useIPv6 { cmd = "traceroute6" } else { cmd = "traceroute" }
args = []string{"-a", "-q1", "-w1", "-m15", query}
} else if runtime.GOOS == "openbsd" {
if useIPv6 { cmd = "traceroute6" } else { cmd = "traceroute" }
args = []string{"-A", "-q1", "-w1", "-m15", query}
} else if runtime.GOOS == "linux" {
cmd = "traceroute"
if useIPv6 {
args = []string{"-6", "-A", "-q1", "-N32", "-w1", "-m15", query}
} else {
args = []string{"-4", "-A", "-q1", "-N32", "-w1", "-m15", query}
}
} else {
httpW.WriteHeader(http.StatusInternalServerError)
httpW.Write([]byte("Traceroute Not Supported\n"))
return
}
instance := exec.Command(cmd, args...)
output, err := instance.Output()
if err != nil {
httpW.WriteHeader(http.StatusInternalServerError)
httpW.Write([]byte("Traceroute Execution Error: "))
httpW.Write([]byte(err.Error() + "\n"))
return
}
httpW.Write(output)
}
}
func main() {
var err error
birdPtr := flag.String("bird", "/var/run/bird/bird.ctl", "socket file for bird")
bird6Ptr := flag.String("bird6", "/var/run/bird/bird6.ctl", "socket file for bird6")
flag.Parse()
// Initialize BIRDv4 socket
bird, err = net.Dial("unix", *birdPtr)
if err != nil {
panic(err)
}
defer bird.Close()
birdReadln(bird, nil)
birdWriteln(bird, "restrict")
birdReadln(bird, nil)
// Initialize BIRDv6 socket
bird6, err = net.Dial("unix", *bird6Ptr)
if err != nil {
panic(err)
}
defer bird6.Close()
birdReadln(bird6, nil)
birdWriteln(bird6, "restrict")
birdReadln(bird6, nil)
// Start HTTP server
http.HandleFunc("/", invalidHandler)
http.HandleFunc("/bird", birdHandler)
http.HandleFunc("/bird6", bird6Handler)
http.HandleFunc("/traceroute", tracerouteIPv4Wrapper)
http.HandleFunc("/traceroute6", tracerouteIPv6Wrapper)
http.ListenAndServe(":8000", nil)
}