diff --git a/.ijwb/.bazelproject b/.ijwb/.bazelproject new file mode 100644 index 0000000..0ff1046 --- /dev/null +++ b/.ijwb/.bazelproject @@ -0,0 +1,18 @@ +directories: + . + +# Automatically includes all relevant targets under the 'directories' above +derive_targets_from_directories: true + +targets: + # If source code isn't resolving, add additional targets that compile it here + +additional_languages: + # Uncomment any additional languages you want supported + # dart + # javascript + # python + # typescript + +build_flags: + --features=pure diff --git a/.ijwb/.blaze/modules/.project-data-dir.iml b/.ijwb/.blaze/modules/.project-data-dir.iml new file mode 100644 index 0000000..cf80c62 --- /dev/null +++ b/.ijwb/.blaze/modules/.project-data-dir.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.ijwb/.blaze/modules/.workspace.iml b/.ijwb/.blaze/modules/.workspace.iml new file mode 100644 index 0000000..70dd7e2 --- /dev/null +++ b/.ijwb/.blaze/modules/.workspace.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.ijwb/.idea/$CACHE_FILE$ b/.ijwb/.idea/$CACHE_FILE$ new file mode 100644 index 0000000..6cb8985 --- /dev/null +++ b/.ijwb/.idea/$CACHE_FILE$ @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.ijwb/.idea/.gitignore b/.ijwb/.idea/.gitignore new file mode 100644 index 0000000..f9a0b04 --- /dev/null +++ b/.ijwb/.idea/.gitignore @@ -0,0 +1,6 @@ +# Default ignored files +/workspace.xml +# Project exclude paths +/. + + diff --git a/.ijwb/.idea/.name b/.ijwb/.idea/.name new file mode 100644 index 0000000..7b66269 --- /dev/null +++ b/.ijwb/.idea/.name @@ -0,0 +1 @@ +navigator \ No newline at end of file diff --git a/.ijwb/.idea/dictionaries/eve.xml b/.ijwb/.idea/dictionaries/eve.xml new file mode 100644 index 0000000..85f8d5c --- /dev/null +++ b/.ijwb/.idea/dictionaries/eve.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.ijwb/.idea/externalDependencies.xml b/.ijwb/.idea/externalDependencies.xml new file mode 100644 index 0000000..133bf3d --- /dev/null +++ b/.ijwb/.idea/externalDependencies.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.ijwb/.idea/misc.xml b/.ijwb/.idea/misc.xml new file mode 100644 index 0000000..dbe0e46 --- /dev/null +++ b/.ijwb/.idea/misc.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.ijwb/.idea/modules.xml b/.ijwb/.idea/modules.xml new file mode 100644 index 0000000..6e46a2e --- /dev/null +++ b/.ijwb/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.ijwb/.idea/runConfigurations.xml b/.ijwb/.idea/runConfigurations.xml new file mode 100644 index 0000000..26bfd41 --- /dev/null +++ b/.ijwb/.idea/runConfigurations.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.ijwb/.idea/vcs.xml b/.ijwb/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/.ijwb/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.ijwb/.idea/watcherTasks.xml b/.ijwb/.idea/watcherTasks.xml new file mode 100644 index 0000000..eedcd2f --- /dev/null +++ b/.ijwb/.idea/watcherTasks.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/BUILD.bazel b/BUILD.bazel index e777585..eb96ae4 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -10,15 +10,16 @@ go_library( importpath = "git.eve.moe/jackyyf/navigator", visibility = ["//visibility:private"], deps = [ + "//api/beacon/v1:go_default_library", + "//api/navigator:go_default_library", "//ipgeo:go_default_library", "//mapping:go_default_library", - "@com_github_ipipdotnet_ipdb_go//:go_default_library", ], ) go_binary( name = "navigator", - pure = "on", + data = glob(["rules/**"]), embed = [":go_default_library"], visibility = ["//visibility:public"], ) diff --git a/api/beacon/BUILD.bazel b/api/beacon/BUILD.bazel new file mode 100644 index 0000000..a6f0a81 --- /dev/null +++ b/api/beacon/BUILD.bazel @@ -0,0 +1,8 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["api.go"], + importpath = "git.eve.moe/jackyyf/navigator/api/beacon", + visibility = ["//visibility:public"], +) diff --git a/api/beacon/api.go b/api/beacon/api.go new file mode 100644 index 0000000..5a53e06 --- /dev/null +++ b/api/beacon/api.go @@ -0,0 +1,18 @@ +package beacon + +import ( + "fmt" + "net/http" +) + +var ( + apiServeMux = http.NewServeMux() +) + +func init() { + http.Handle("/beacon/", http.StripPrefix("/beacon", apiServeMux)) +} + +func RegisterApi(version string, handler http.Handler) { + apiServeMux.Handle(fmt.Sprintf("/%s/", version), http.StripPrefix(fmt.Sprint("/", version), handler)) +} diff --git a/api/beacon/v1/BUILD.bazel b/api/beacon/v1/BUILD.bazel new file mode 100644 index 0000000..5acba9f --- /dev/null +++ b/api/beacon/v1/BUILD.bazel @@ -0,0 +1,13 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["api.go"], + importpath = "git.eve.moe/jackyyf/navigator/api/beacon/v1", + visibility = ["//visibility:public"], + deps = [ + "//api/beacon:go_default_library", + "//mapping:go_default_library", + "//utils:go_default_library", + ], +) diff --git a/api/beacon/v1/api.go b/api/beacon/v1/api.go new file mode 100644 index 0000000..9d914aa --- /dev/null +++ b/api/beacon/v1/api.go @@ -0,0 +1,33 @@ +package v1 + +import ( + "encoding/json" + "git.eve.moe/jackyyf/navigator/api/beacon" + "git.eve.moe/jackyyf/navigator/mapping" + "git.eve.moe/jackyyf/navigator/utils" + "net/http" +) + +var ( + serveMux = http.NewServeMux() +) + +func init() { + serveMux.HandleFunc("/getNodes", func(resp http.ResponseWriter, req *http.Request) { + host := utils.GetRemoteIP(req) + nodes := mapping.GetNodes() + if nodes == nil { + utils.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, + }) + }) + beacon.RegisterApi("v1", serveMux) +} diff --git a/api/navigator/BUILD.bazel b/api/navigator/BUILD.bazel new file mode 100644 index 0000000..0267039 --- /dev/null +++ b/api/navigator/BUILD.bazel @@ -0,0 +1,13 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["api.go"], + importpath = "git.eve.moe/jackyyf/navigator/api/navigator", + visibility = ["//visibility:public"], + deps = [ + "//ipgeo:go_default_library", + "//mapping:go_default_library", + "//utils:go_default_library", + ], +) diff --git a/api/navigator/api.go b/api/navigator/api.go new file mode 100644 index 0000000..70b565b --- /dev/null +++ b/api/navigator/api.go @@ -0,0 +1,109 @@ +package navigator + +import ( + "encoding/json" + "fmt" + "git.eve.moe/jackyyf/navigator/ipgeo" + "git.eve.moe/jackyyf/navigator/mapping" + "git.eve.moe/jackyyf/navigator/utils" + "log" + "net" + "net/http" + "strings" +) + +const ( + errIPv4Only = "Navigator works for valid IPv4 only :)" +) + +func init() { + http.HandleFunc("/healthz", func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(200) + resp.Write([]byte("ok")) + }) + + http.HandleFunc("/info", func(resp http.ResponseWriter, req *http.Request) { + var host string + if argIp := req.FormValue("ip"); argIp != "" { + host = argIp + if net.ParseIP(host).To4() == nil { + utils.ResponseWithError(resp, http.StatusPreconditionFailed, errIPv4Only) + return + } + } else { + host = utils.GetRemoteIP(req) + } + db := ipgeo.Get() + + info_cn, err := db.FindInfo(host, "CN") + if err != nil { + fmt.Fprintf(resp, "IP %s not found in the database.", host) + return + } + info_en, err := db.FindInfo(host, "EN") + if err != nil { + fmt.Fprintf(resp, "IP %s not found in the database.", host) + return + } + resp.Header().Set("Content-Type", "text/plain") + resp.WriteHeader(http.StatusOK) + server := mapping.Get(host) + fmt.Fprintln(resp, "您的IP:", host) + fmt.Fprintln(resp, "数据库中IP所属位置:", utils.BuildLocation(info_cn)) + if info_cn.IspDomain != "" { + fmt.Fprintln(resp, "数据库中IP所属运营商:", info_cn.IspDomain) + } + fmt.Fprintln(resp, "您被分配的CDN节点为:", server) + fmt.Fprintln(resp) + fmt.Fprintln(resp, strings.Repeat("=", 72)) + + fmt.Fprintln(resp, "Your IP:", host) + fmt.Fprintln(resp, "Location for your IP according our database:", utils.BuildLocation(info_en)) + if info_en.IspDomain != "" { + fmt.Fprintln(resp, "ISP for your IP according our database:", info_en.IspDomain) + } + fmt.Fprintln(resp, "Allocated CDN node for you:", server) + fmt.Fprintln(resp) + }) + + http.HandleFunc("/mapping", func(resp http.ResponseWriter, req *http.Request) { + host := utils.GetRemoteIP(req) + resp.Header().Set("Content-Type", "text/plain") + resp.WriteHeader(http.StatusOK) + server := mapping.Get(host) + fmt.Fprint(resp, server) + }) + http.HandleFunc("/getMapping", func(resp http.ResponseWriter, req *http.Request) { + ip := req.FormValue("ip") + if net.ParseIP(ip).To4() == nil { + utils.ResponseWithError(resp, http.StatusBadRequest, errIPv4Only) + return + } + resp.Header().Set("Content-Type", "text/plain") + resp.WriteHeader(http.StatusOK) + server := mapping.Get(ip) + 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 { + utils.ResponseWithError(resp, http.StatusBadRequest, errIPv4Only) + return + } + nodes := mapping.GetNodes() + if nodes == nil { + utils.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) + }) +} diff --git a/main.go b/main.go index 1bf14ff..dce42c9 100644 --- a/main.go +++ b/main.go @@ -1,197 +1,24 @@ package main import ( - "encoding/json" "flag" - "fmt" - "log" - "net" - "net/http" - _ "net/http/pprof" - "strings" - + _ "git.eve.moe/jackyyf/navigator/api/beacon/v1" + _ "git.eve.moe/jackyyf/navigator/api/navigator" "git.eve.moe/jackyyf/navigator/ipgeo" "git.eve.moe/jackyyf/navigator/mapping" - "github.com/ipipdotnet/ipdb-go" + "log" + "net/http" + _ "net/http/pprof" ) -const ( - errIPv4Only = "Navigator works for valid IPv4 only :)" - remoteAddrHeader = "X-NAV-REMOTE-IP" -) - -type errorMessage struct { - Error string `json:error` -} - var ( listen_spec = flag.String("bind", "127.0.0.1:8086", "http server bind spec") ) -func responseWithError(resp http.ResponseWriter, statusCode int, message string) { - resp.Header().Set("Content-Type", "text/plain") - resp.WriteHeader(statusCode) - resp.Write([]byte("error: " + message)) -} - -func responseWithJsonError(resp http.ResponseWriter, statusCode int, message string) { - resp.Header().Set("Content-Type", "application/json") - 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") - } -} - -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 { - ret := "" - if info.CountryName != "" { - ret += info.CountryName + " " - } - if info.RegionName != "" { - ret += info.RegionName + " " - } - if info.CityName != "" { - ret += info.CityName + " " - } - return strings.TrimSpace(ret) -} - func main() { flag.Parse() ipgeo.Initialize() mapping.Initialize() - - http.HandleFunc("/healthz", func(resp http.ResponseWriter, req *http.Request) { - resp.WriteHeader(200) - resp.Write([]byte("ok")) - }) - - http.HandleFunc("/info", func(resp http.ResponseWriter, req *http.Request) { - var host string - if argIp := req.FormValue("ip"); argIp != "" { - host = argIp - if net.ParseIP(host).To4() == nil { - responseWithError(resp, http.StatusPreconditionFailed, errIPv4Only) - return - } - } else { - host = getRemoteIP(req) - } - db := ipgeo.Get() - - info_cn, err := db.FindInfo(host, "CN") - if err != nil { - fmt.Fprintf(resp, "IP %s not found in the database.", host) - return - } - info_en, err := db.FindInfo(host, "EN") - if err != nil { - fmt.Fprintf(resp, "IP %s not found in the database.", host) - return - } - resp.Header().Set("Content-Type", "text/plain") - resp.WriteHeader(http.StatusOK) - server := mapping.Get(host) - fmt.Fprintln(resp, "您的IP:", host) - fmt.Fprintln(resp, "数据库中IP所属位置:", buildLocation(info_cn)) - if info_cn.IspDomain != "" { - fmt.Fprintln(resp, "数据库中IP所属运营商:", info_cn.IspDomain) - } - fmt.Fprintln(resp, "您被分配的CDN节点为:", server) - fmt.Fprintln(resp) - fmt.Fprintln(resp, strings.Repeat("=", 72)) - - fmt.Fprintln(resp, "Your IP:", host) - fmt.Fprintln(resp, "Location for your IP according our database:", buildLocation(info_en)) - if info_en.IspDomain != "" { - fmt.Fprintln(resp, "ISP for your IP according our database:", info_en.IspDomain) - } - fmt.Fprintln(resp, "Allocated CDN node for you:", server) - fmt.Fprintln(resp) - }) - - http.HandleFunc("/mapping", func(resp http.ResponseWriter, req *http.Request) { - host := getRemoteIP(req) - resp.Header().Set("Content-Type", "text/plain") - resp.WriteHeader(http.StatusOK) - server := mapping.Get(host) - fmt.Fprint(resp, server) - }) - http.HandleFunc("/getMapping", func(resp http.ResponseWriter, req *http.Request) { - ip := req.FormValue("ip") - if net.ParseIP(ip).To4() == nil { - responseWithError(resp, http.StatusBadRequest, errIPv4Only) - return - } - resp.Header().Set("Content-Type", "text/plain") - resp.WriteHeader(http.StatusOK) - server := mapping.Get(ip) - 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) } diff --git a/mapping/elf/core.go b/mapping/elf/core.go index 1387d88..6c26e24 100644 --- a/mapping/elf/core.go +++ b/mapping/elf/core.go @@ -143,14 +143,7 @@ func reload() { parsedFunc = &globals } -func GetMapping(ip string) string { - thread := pool.Get() - ret, err := starlark.Call(thread, (*parsedFunc)["getMapping"], - starlark.Tuple{starlark.String(ip)}, nil) - if err != nil { - log.Println("Starlark execute error:", err.Error()) - return "" - } +func getTarget(ret starlark.Value) string { switch r := ret.(type) { case starlark.String: return string(r) @@ -205,6 +198,17 @@ scriptError: return "" } +func GetMapping(ip string) string { + thread := pool.Get() + ret, err := starlark.Call(thread, (*parsedFunc)["getMapping"], + starlark.Tuple{starlark.String(ip)}, nil) + if err != nil { + log.Println("Starlark execute error:", err.Error()) + return "" + } + return getTarget(ret) +} + func GetNodes() (res []string) { thread := pool.Get() ret, err := starlark.Call(thread, (*parsedFunc)["getNodes"], nil, nil) @@ -215,12 +219,11 @@ func GetNodes() (res []string) { 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 { + res[i] = getTarget(r.Index(i)) + if res[i] == "" { log.Println("Script returned unexpected result:", ret.String()) + return nil } } } else { diff --git a/rules/map.starlark b/rules/map.starlark index bd444c0..9e09059 100644 --- a/rules/map.starlark +++ b/rules/map.starlark @@ -3,7 +3,9 @@ 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) +CLOUDCONE_LAX3_1GE = "ge-lax3-us" +HOSTSOLUTIONS_OMR1_1GE = "ge-omr1-ro" +CLOUDCONE_LAX_LB = (CLOUDCONE_LAX1_1GE, CLOUDCONE_LAX2_1GE, CLOUDCONE_LAX3_1GE) default_server = WHOLESALE_INTERNET_10GE CHINA_MAINLAND_SUFFIX = ".eveedge.link" @@ -30,7 +32,7 @@ def getMapping(ip): return default_server def getNodes(): - return ["xe-mci1-us", "ge-fsn1-de", "ge-lax1-us"] + return [WHOLESALE_INTERNET_10GE, HETZNER_FSN_1GE, CLOUDCONE_LAX_LB, HOSTSOLUTIONS_OMR1_1GE] def getSuffix(ip): info = geoLookup(ip) diff --git a/utils/BUILD.bazel b/utils/BUILD.bazel new file mode 100644 index 0000000..8034a1e --- /dev/null +++ b/utils/BUILD.bazel @@ -0,0 +1,9 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["utils.go"], + importpath = "git.eve.moe/jackyyf/navigator/utils", + visibility = ["//visibility:public"], + deps = ["@com_github_ipipdotnet_ipdb_go//:go_default_library"], +) diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..2a042dd --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,67 @@ +package utils + +import ( + "encoding/json" + "github.com/ipipdotnet/ipdb-go" + "net" + "net/http" + "strings" +) + +const ( + remoteAddrHeader = "X-NAV-REMOTE-IP" +) + +type errorMessage struct { + Error string `json:"error"` +} + +func ResponseWithError(resp http.ResponseWriter, statusCode int, message string) { + resp.Header().Set("Content-Type", "text/plain") + resp.WriteHeader(statusCode) + resp.Write([]byte("error: " + message)) +} + +func ResponseWithJsonError(resp http.ResponseWriter, statusCode int, message string) { + resp.Header().Set("Content-Type", "application/json") + 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") + } +} + +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 { + ret := "" + if info.CountryName != "" { + ret += info.CountryName + " " + } + if info.RegionName != "" { + ret += info.RegionName + " " + } + if info.CityName != "" { + ret += info.CityName + " " + } + return strings.TrimSpace(ret) +}