4 Commits

10 changed files with 303 additions and 31 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/bazel-*

24
BUILD.bazel Normal file
View File

@ -0,0 +1,24 @@
load("@bazel_gazelle//:def.bzl", "gazelle")
gazelle(name = "gazelle")
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "git.eve.moe/jackyyf/navigator",
visibility = ["//visibility:private"],
deps = [
"//ipgeo:go_default_library",
"//mapping:go_default_library",
"@com_github_ipipdotnet_ipdb_go//:go_default_library",
],
)
go_binary(
name = "navigator",
pure = "on",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)

43
WORKSPACE Normal file
View File

@ -0,0 +1,43 @@
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "io_bazel_rules_go",
urls = [
"https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/v0.20.3/rules_go-v0.20.3.tar.gz",
"https://github.com/bazelbuild/rules_go/releases/download/v0.20.3/rules_go-v0.20.3.tar.gz",
],
sha256 = "e88471aea3a3a4f19ec1310a55ba94772d087e9ce46e41ae38ecebe17935de7b",
)
http_archive(
name = "bazel_gazelle",
urls = [
"https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/bazel-gazelle/releases/download/v0.19.1/bazel-gazelle-v0.19.1.tar.gz",
"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.19.1/bazel-gazelle-v0.19.1.tar.gz",
],
sha256 = "86c6d481b3f7aedc1d60c1c211c6f76da282ae197c3b3160f54bd3a8f847896f",
)
load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains")
go_rules_dependencies()
go_register_toolchains()
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
gazelle_dependencies()
go_repository(
name = "com_github_ipipdotnet_ipdb_go",
importpath = "github.com/ipipdotnet/ipdb-go",
sum = "h1:Afa0qx/SgRevzIK8Qg1TevuD5M28kFLWbzPvU+GQJ08=",
version = "v1.2.0",
)
go_repository(
name = "net_starlark_go",
importpath = "go.starlark.net",
sum = "h1:ZP11CRSzO9uOTTOVkH6yodtI3kSY69vUID8lx8B0M3s=",
version = "v0.0.0-20191113183327-aaf7be003892",
)

14
ipgeo/BUILD.bazel Normal file
View File

@ -0,0 +1,14 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["ipdb.go"],
importpath = "git.eve.moe/jackyyf/navigator/ipgeo",
visibility = ["//visibility:public"],
deps = [
"//mapping/elf:go_default_library",
"@com_github_ipipdotnet_ipdb_go//:go_default_library",
"@net_starlark_go//starlark:go_default_library",
"@net_starlark_go//starlarkstruct:go_default_library",
],
)

90
main.go
View File

@ -8,7 +8,6 @@ import (
"net"
"net/http"
_ "net/http/pprof"
"strconv"
"strings"
"git.eve.moe/jackyyf/navigator/ipgeo"
@ -17,7 +16,8 @@ import (
)
const (
errIPv4Only = "Navigator works for valid IPv4 only :)"
errIPv4Only = "Navigator works for valid IPv4 only :)"
remoteAddrHeader = "X-NAV-REMOTE-IP"
)
type errorMessage struct {
@ -36,16 +36,32 @@ func responseWithError(resp http.ResponseWriter, statusCode int, message string)
func responseWithJsonError(resp http.ResponseWriter, statusCode int, message string) {
resp.Header().Set("Content-Type", "application/json")
body, err := json.Marshal(&errorMessage{
resp.WriteHeader(statusCode)
encoder := json.NewEncoder(resp)
err := encoder.Encode(&errorMessage{
Error: message,
})
if err != nil {
// This should never happen
panic("json marshal failed, check code")
}
resp.Header().Set("Content-Length", strconv.Itoa(len(body)))
resp.WriteHeader(statusCode)
resp.Write(body)
}
func getRemoteIP(req *http.Request) string {
if addr := req.Header.Get(remoteAddrHeader); addr != "" {
if net.ParseIP(addr).To4() == nil {
return ""
}
return addr
}
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return ""
}
if net.ParseIP(host).To4() == nil {
return ""
}
return host
}
func buildLocation(info *ipdb.CityInfo) string {
@ -76,17 +92,12 @@ func main() {
var host string
if argIp := req.FormValue("ip"); argIp != "" {
host = argIp
} else {
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
if net.ParseIP(host).To4() == nil {
responseWithError(resp, http.StatusPreconditionFailed, errIPv4Only)
return
}
host = ip
}
if net.ParseIP(host).To4() == nil {
responseWithError(resp, http.StatusPreconditionFailed, errIPv4Only)
return
} else {
host = getRemoteIP(req)
}
db := ipgeo.Get()
@ -122,15 +133,7 @@ func main() {
})
http.HandleFunc("/mapping", func(resp http.ResponseWriter, req *http.Request) {
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
responseWithError(resp, http.StatusPreconditionFailed, errIPv4Only)
return
}
if net.ParseIP(host).To4() == nil {
responseWithError(resp, http.StatusPreconditionFailed, errIPv4Only)
return
}
host := getRemoteIP(req)
resp.Header().Set("Content-Type", "text/plain")
resp.WriteHeader(http.StatusOK)
server := mapping.Get(host)
@ -148,6 +151,47 @@ func main() {
log.Printf("%s => %s\n", ip, server)
fmt.Fprint(resp, server)
})
http.HandleFunc("/getNodes", func(resp http.ResponseWriter, req *http.Request) {
ip := req.FormValue("ip")
if net.ParseIP(ip).To4() == nil {
responseWithError(resp, http.StatusBadRequest, errIPv4Only)
return
}
nodes := mapping.GetNodes()
if nodes == nil {
responseWithJsonError(resp, http.StatusInternalServerError, "Unable to get nodes")
return
}
suffix := mapping.GetSuffix(ip)
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(http.StatusOK)
jsonEncoder := json.NewEncoder(resp)
ret := make([]string, 0, len(nodes))
for _, node := range nodes {
ret = append(ret, node+suffix)
}
jsonEncoder.Encode(ret)
})
clientApi := http.NewServeMux()
http.Handle("/client/", http.StripPrefix("/client", clientApi))
clientV1Api := http.NewServeMux()
clientApi.Handle("/v1/", http.StripPrefix("/v1", clientV1Api))
clientV1Api.HandleFunc("/getNodes", func(resp http.ResponseWriter, req *http.Request) {
host := getRemoteIP(req)
nodes := mapping.GetNodes()
if nodes == nil {
responseWithJsonError(resp, http.StatusInternalServerError, "Unable to get nodes")
return
}
suffix := mapping.GetSuffix(host)
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(http.StatusOK)
jsonEncoder := json.NewEncoder(resp)
jsonEncoder.Encode(map[string]interface{}{
"nodes": nodes,
"suffix": suffix,
})
})
log.Println("HTTP server is running on", *listen_spec)
http.ListenAndServe(*listen_spec, nil)
}

9
mapping/BUILD.bazel Normal file
View File

@ -0,0 +1,9 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["map.go"],
importpath = "git.eve.moe/jackyyf/navigator/mapping",
visibility = ["//visibility:public"],
deps = ["//mapping/elf:go_default_library"],
)

9
mapping/elf/BUILD.bazel Normal file
View File

@ -0,0 +1,9 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["core.go"],
importpath = "git.eve.moe/jackyyf/navigator/mapping/elf",
visibility = ["//visibility:public"],
deps = ["@net_starlark_go//starlark:go_default_library"],
)

View File

@ -4,11 +4,14 @@ import (
"flag"
"fmt"
"log"
"math/rand"
"os"
"os/signal"
"sort"
"sync"
"sync/atomic"
"syscall"
"time"
"go.starlark.net/starlark"
)
@ -27,6 +30,7 @@ var (
requiredFunc = map[string]struct{}{
"getMapping": struct{}{},
"getNodes": struct{}{},
"getSuffix": struct{}{},
}
parsedFunc *starlark.StringDict
parseThread = &starlark.Thread{
@ -35,6 +39,7 @@ var (
reloadSignal = make(chan os.Signal)
pool *threadPool
counter uint64
rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
)
func newThreadPool(size int) *threadPool {
@ -146,6 +151,92 @@ func GetMapping(ip string) string {
log.Println("Starlark execute error:", err.Error())
return ""
}
switch r := ret.(type) {
case starlark.String:
return string(r)
case *starlark.List:
val := r.Index(rnd.Intn(r.Len()))
if s, ok := val.(starlark.String); ok {
return string(s)
}
goto scriptError
case starlark.Tuple:
val := r.Index(rnd.Intn(r.Len()))
if s, ok := val.(starlark.String); ok {
return string(s)
}
goto scriptError
case *starlark.Dict:
values := r.Items()
options := make([]string, r.Len())
tot := int64(0)
for i := range values {
if v, ok := values[i].Index(0).(starlark.String); ok {
options[i] = string(v)
} else {
goto scriptError
}
if v, ok := values[i].Index(1).(starlark.Int); ok {
if weight, ok := v.Int64(); ok {
tot += weight
} else {
goto scriptError
}
} else {
goto scriptError
}
}
sort.Strings(options)
pos := rnd.Int63n(tot)
for _, option := range options {
w, _, _ := r.Get(starlark.String(option))
v, _ := w.(starlark.Int).Int64()
pos -= v
if pos < 0 {
return option
}
}
log.Println("Unexpected code execution,",
"check random logic for *starlark.Dict")
return options[len(options)-1]
}
scriptError:
log.Println("Script returned unexpected result:", ret.String())
return ""
}
func GetNodes() (res []string) {
thread := pool.Get()
ret, err := starlark.Call(thread, (*parsedFunc)["getNodes"], nil, nil)
if err != nil {
log.Println("Starlark execute error:", err.Error())
return nil
}
if r, ok := ret.(*starlark.List); ok {
res = make([]string, r.Len())
for i := 0; i < r.Len(); i++ {
v := r.Index(i)
if s, ok := v.(starlark.String); ok {
res[i] = string(s)
} else {
log.Println("Script returned unexpected result:",
ret.String())
}
}
} else {
log.Println("Script returned unexpected result:", ret.String())
}
return
}
func GetSuffix(ip string) string {
thread := pool.Get()
ret, err := starlark.Call(thread, (*parsedFunc)["getSuffix"],
starlark.Tuple{starlark.String(ip)}, nil)
if err != nil {
log.Println("Starlark execute error:", err.Error())
return ""
}
if r, ok := ret.(starlark.String); ok {
return string(r)
}

View File

@ -7,8 +7,10 @@ import (
var (
defaultServer = flag.String("fallback-node",
"xe-mci1-us.edge.eve.network",
"Default CDN node in case of any error when executing script")
"xe-mci1-us", "Default CDN node in case of any script error")
defaultSuffix = flag.String("fallback-suffix",
".edge.eve.network",
"Default CDN suffix in case of any script error")
)
func Initialize() {
@ -16,9 +18,25 @@ func Initialize() {
}
func Get(ip string) string {
ret := elf.GetMapping(ip)
node := elf.GetMapping(ip)
if node == "" {
node = *defaultServer
}
suffix := elf.GetSuffix(ip)
if suffix == "" {
suffix = *defaultSuffix
}
return node + suffix
}
func GetSuffix(ip string) string {
ret := elf.GetSuffix(ip)
if ret == "" {
return *defaultServer
return *defaultSuffix
}
return ret
}
func GetNodes() []string {
return elf.GetNodes()
}

View File

@ -1,6 +1,9 @@
WHOLESALE_INTERNET_10GE = "xe-mci1-us"
HETZNER_FSN_1GE = "ge-fsn1-de"
HETZNER_HEL_1GE = "ge-hel1-fi"
CLOUDCONE_LAX1_1GE = "ge-lax1-us"
CLOUDCONE_LAX2_1GE = "ge-lax2-us"
CLOUDCONE_LAX_LB = (CLOUDCONE_LAX1_1GE, CLOUDCONE_LAX2_1GE)
default_server = WHOLESALE_INTERNET_10GE
CHINA_MAINLAND_SUFFIX = ".eveedge.link"
@ -11,12 +14,28 @@ default_suffix = GLOBAL_SUFFIX
def getMapping(ip):
info = geoLookup(ip)
if not info:
return default_server + default_suffix
return default_server
if info.IspDomain == "ChinaMobile":
return HETZNER_FSN_1GE + CHINA_MAINLAND_SUFFIX
return HETZNER_FSN_1GE
if info.IspDomain == "ChinaTelecom":
return CLOUDCONE_LAX_LB
if info.CountryCode == "CN":
return default_server + CHINA_MAINLAND_SUFFIX
return default_server + GLOBAL_SUFFIX
return default_server
if ip == "0.11.45.14":
return {
default_server: 3,
CLOUDCONE_LAX1_1GE: 2,
CLOUDCONE_LAX2_1GE: 1,
}
return default_server
def getNodes():
return ["xe-mci1-us", "ge-fsn1-de", "ge-lax1-us"]
def getSuffix(ip):
info = geoLookup(ip)
if not info:
return default_suffix
if info.CountryCode == "CN":
return CHINA_MAINLAND_SUFFIX
return GLOBAL_SUFFIX