feat!: split ui and api backend
This commit is contained in:
parent
50ff583672
commit
b88b96546f
11 changed files with 243 additions and 250 deletions
173
api/main.go
Normal file
173
api/main.go
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/oschwald/geoip2-golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
asnDB *geoip2.Reader
|
||||||
|
cityDB *geoip2.Reader
|
||||||
|
config serverConfig
|
||||||
|
cache sync.Map
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
devMode = "dev"
|
||||||
|
prodMode = "prod"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverConfig struct {
|
||||||
|
ASNDB string
|
||||||
|
CityDB string
|
||||||
|
Listen string
|
||||||
|
Mode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type body struct {
|
||||||
|
IP net.IP `json:"ip"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
City string `json:"city"`
|
||||||
|
ASN uint `json:"asn"`
|
||||||
|
Organization string `json:"organization"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type bodyError struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config = serverConfig{
|
||||||
|
ASNDB: os.Getenv("IP_CHECKER_ASN_DB"),
|
||||||
|
CityDB: os.Getenv("IP_CHECKER_CITY_DB"),
|
||||||
|
Listen: getEnvOr("IP_CHECKER_LISTEN", ":8080"),
|
||||||
|
Mode: os.Getenv("IP_CHECKER_MODE"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.CityDB != "" {
|
||||||
|
var err error
|
||||||
|
cityDB, err = geoip2.Open(config.CityDB)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error opening GeoIP City database: %v", err)
|
||||||
|
}
|
||||||
|
defer cityDB.Close()
|
||||||
|
} else {
|
||||||
|
log.Fatalln("'IP_CHECKER_CITY_DB' not set")
|
||||||
|
}
|
||||||
|
if config.ASNDB != "" {
|
||||||
|
var err error
|
||||||
|
asnDB, err = geoip2.Open(config.ASNDB)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error opening GeoIP ASN database: %v", err)
|
||||||
|
}
|
||||||
|
defer asnDB.Close()
|
||||||
|
} else {
|
||||||
|
log.Fatalln("'IP_CHECKER_ASN_DB' not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/api/v1/ip", handleRequest)
|
||||||
|
log.Printf("Starting server on %s", config.Listen)
|
||||||
|
log.Fatal(http.ListenAndServe(config.Listen, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateJSON(ip string) (body, error) {
|
||||||
|
var data body
|
||||||
|
|
||||||
|
parsedIP := net.ParseIP(ip)
|
||||||
|
|
||||||
|
// check for reserved ip
|
||||||
|
if parsedIP == nil {
|
||||||
|
return body{}, errors.New("invalid IP")
|
||||||
|
} else if parsedIP.IsPrivate() {
|
||||||
|
return body{}, errors.New("private IP")
|
||||||
|
} else if parsedIP.IsLoopback() {
|
||||||
|
return body{}, errors.New("loopback IP")
|
||||||
|
} else if parsedIP.IsUnspecified() {
|
||||||
|
return body{}, errors.New("unspecified IP")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache
|
||||||
|
if cached, found := cache.Load(ip); found && config.Mode == prodMode {
|
||||||
|
return cached.(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// query
|
||||||
|
cityRecord, err := cityDB.City(parsedIP)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("GeoIP City lookup failed for IP: %s, error: %v", ip, err)
|
||||||
|
return body{}, errors.New("City record lookup failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
ASNRecord, err := asnDB.ASN(parsedIP)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("GeoIP ASN lookup failed for IP: %s, error: %v", ip, err)
|
||||||
|
return body{}, errors.New("ASN record lookup failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
data.IP = parsedIP
|
||||||
|
data.Country = cityRecord.Country.Names["en"]
|
||||||
|
data.Region = cityRecord.Subdivisions[0].Names["en"]
|
||||||
|
data.City = cityRecord.City.Names["en"]
|
||||||
|
data.ASN = ASNRecord.AutonomousSystemNumber
|
||||||
|
data.Organization = ASNRecord.AutonomousSystemOrganization
|
||||||
|
|
||||||
|
if config.Mode == prodMode {
|
||||||
|
cache.Store(ip, data)
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sourceIP := func() string {
|
||||||
|
if q := r.URL.Query().Get("q"); q != "" {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Mode == "dev" {
|
||||||
|
return "223.5.5.5"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
|
||||||
|
return strings.Split(ip, ",")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
errorText := fmt.Sprintf("Unable to parse remote address: %v", err)
|
||||||
|
log.Printf(errorText)
|
||||||
|
return errorText
|
||||||
|
}
|
||||||
|
|
||||||
|
return host
|
||||||
|
}()
|
||||||
|
|
||||||
|
bodyData, err := generateJSON(sourceIP)
|
||||||
|
if err != nil {
|
||||||
|
errorBody := bodyError{
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
errorData, _ := json.MarshalIndent(errorBody, "", " ")
|
||||||
|
http.Error(w, string(errorData)+"\n", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bodyJson, _ := json.MarshalIndent(bodyData, "", " ")
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s\n", bodyJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOr(key string, defaultValue string) string {
|
||||||
|
if value, exists := os.LookupEnv(key); exists {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
|
@ -1,50 +0,0 @@
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
const [geoIP, speedTest, ipApiIs] = await Promise.all([
|
|
||||||
fetchAsync("https://api.ip.sb/geoip"),
|
|
||||||
fetchAsync("https://api-v3.speedtest.cn/ip"),
|
|
||||||
fetchAsync("https://api.ipapi.is/"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Update DOM for IP.sb
|
|
||||||
updateDom("check-ipsb-ip", geoIP.ip);
|
|
||||||
updateDom("check-ipsb-location", [geoIP.city, geoIP.country, geoIP.isp]);
|
|
||||||
|
|
||||||
// Update DOM for SpeedTest.cn
|
|
||||||
const { ip, city, country, isp } = speedTest.data;
|
|
||||||
updateDom("check-speedtestcn-ip", ip);
|
|
||||||
updateDom("check-speedtestcn-location", [city, country, isp]);
|
|
||||||
|
|
||||||
// Update DOM for IPAPI.is
|
|
||||||
const {
|
|
||||||
ip: ipapiIP,
|
|
||||||
location: { city: ipapiCity, country: ipapiCountry },
|
|
||||||
company: { name: companyName },
|
|
||||||
} = ipApiIs;
|
|
||||||
updateDom("check-ipapiis-ip", ipapiIP);
|
|
||||||
updateDom("check-ipapiis-location", [ipapiCity, ipapiCountry, companyName]);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error fetching data:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAsync(url) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Fetch error:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDom(elementId, content) {
|
|
||||||
document.getElementById(elementId).innerHTML = Array.isArray(content)
|
|
||||||
? content.join(", ")
|
|
||||||
: content;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = main;
|
|
195
main.go
195
main.go
|
@ -1,195 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/oschwald/geoip2-golang"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ASNDB *geoip2.Reader
|
|
||||||
CityDB *geoip2.Reader
|
|
||||||
tmpl *template.Template
|
|
||||||
config ServerConfig
|
|
||||||
ipCache sync.Map
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServerConfig struct {
|
|
||||||
ASNDB string
|
|
||||||
CityDB string
|
|
||||||
Listen string
|
|
||||||
Mode string
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
config = ServerConfig{
|
|
||||||
ASNDB: os.Getenv("IP_CHECKER_ASN_DB"),
|
|
||||||
CityDB: os.Getenv("IP_CHECKER_CITY_DB"),
|
|
||||||
Listen: getEnvOr("IP_CHECKER_LISTEN", ":8080"),
|
|
||||||
Mode: os.Getenv("IP_CHECKER_MODE"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.CityDB != "" {
|
|
||||||
var err error
|
|
||||||
CityDB, err = geoip2.Open(config.CityDB)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error opening GeoIP City database: %v", err)
|
|
||||||
}
|
|
||||||
defer CityDB.Close()
|
|
||||||
}
|
|
||||||
if config.ASNDB != "" {
|
|
||||||
var err error
|
|
||||||
ASNDB, err = geoip2.Open(config.ASNDB)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error opening GeoIP ASN database: %v", err)
|
|
||||||
}
|
|
||||||
defer ASNDB.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl = template.Must(template.ParseFiles("assets/index.html"))
|
|
||||||
|
|
||||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./assets/static"))))
|
|
||||||
|
|
||||||
http.HandleFunc("/{$}", handleRequest)
|
|
||||||
|
|
||||||
http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintf(w, `User-Agent: *
|
|
||||||
Disallow: /harming/humans
|
|
||||||
Disallow: /ignoring/human/orders
|
|
||||||
Disallow: /harm/to/self
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Printf("Starting server on %s", config.Listen)
|
|
||||||
log.Fatal(http.ListenAndServe(config.Listen, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIPCountry(ip string) string {
|
|
||||||
parsedIP := net.ParseIP(ip)
|
|
||||||
|
|
||||||
// check for reserved ip
|
|
||||||
if parsedIP.IsPrivate() {
|
|
||||||
return "private IP"
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedIP.IsLoopback() {
|
|
||||||
return "loopback IP"
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedIP == nil {
|
|
||||||
log.Printf("Invalid IP address: %s", ip)
|
|
||||||
return "invalid IP"
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if DB exists
|
|
||||||
// FIXME:
|
|
||||||
if config.CityDB == "" && config.ASNDB == "" {
|
|
||||||
log.Printf("GeoIP database not set. Returning 'unknown'")
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache
|
|
||||||
if cached, found := ipCache.Load(ip); found {
|
|
||||||
return cached.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse DB
|
|
||||||
cityRecord, err := CityDB.City(parsedIP)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("GeoIP City lookup failed for IP: %s, error: %v", ip, err)
|
|
||||||
return "City record lookup failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
ASNRecord, err := ASNDB.ASN(parsedIP)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("GeoIP ASN lookup failed for IP: %s, error: %v", ip, err)
|
|
||||||
return "ASN record lookup failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse text
|
|
||||||
country, ok := cityRecord.Country.Names["en"]
|
|
||||||
if !ok {
|
|
||||||
log.Printf("Country name not found in GeoIP data for IP: %s", ip)
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
region, ok := cityRecord.Subdivisions[0].Names["en"]
|
|
||||||
if !ok {
|
|
||||||
log.Printf("Region name not found in GeoIP data for IP: %s", ip)
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
ASN := ASNRecord.AutonomousSystemOrganization
|
|
||||||
if ASN == "" {
|
|
||||||
log.Printf("ASN name not found in GeoIP data for IP: %s", ip)
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
result := region + ", " + country + ", " + ASN
|
|
||||||
ipCache.Store(ip, result)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sourceIP := func() string {
|
|
||||||
if config.Mode == "dev" {
|
|
||||||
return "223.5.5.5"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
|
|
||||||
return strings.Split(ip, ",")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
errorText := fmt.Sprintf("Unable to parse remote address: %v", err)
|
|
||||||
log.Printf(errorText)
|
|
||||||
return errorText
|
|
||||||
}
|
|
||||||
|
|
||||||
return host
|
|
||||||
}()
|
|
||||||
|
|
||||||
isHeadless := strings.HasPrefix(r.Header.Get("User-Agent"), "curl/")
|
|
||||||
|
|
||||||
log.Printf("r.URL.Path: %s, sourceIP: %s, isHeadless: %t, User-Agent: %s", r.URL.Path, sourceIP, isHeadless, r.Header.Get("User-Agent"))
|
|
||||||
|
|
||||||
if isHeadless {
|
|
||||||
fmt.Fprintf(w, "%s\n", sourceIP)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
csp := []string{
|
|
||||||
"default-src 'none'",
|
|
||||||
"img-src 'self'",
|
|
||||||
"script-src-elem 'self'",
|
|
||||||
"style-src-elem 'self' fonts.googleapis.com",
|
|
||||||
"font-src fonts.gstatic.com",
|
|
||||||
"connect-src api.ip.sb api-v3.speedtest.cn api.ipapi.is",
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Security-Policy", strings.Join(csp, "; "))
|
|
||||||
|
|
||||||
data := map[string]string{
|
|
||||||
"sourceIP": sourceIP,
|
|
||||||
"sourceCountry": getIPCountry(sourceIP),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tmpl.Execute(w, data)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to load template", http.StatusInternalServerError)
|
|
||||||
log.Printf("Template execution error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getEnvOr(key, defaultValue string) string {
|
|
||||||
if value, exists := os.LookupEnv(key); exists {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ buildGoModule {
|
||||||
pname = "ip-checker";
|
pname = "ip-checker";
|
||||||
version = "dev";
|
version = "dev";
|
||||||
|
|
||||||
src = ./.;
|
src = ./api;
|
||||||
|
|
||||||
vendorHash = "sha256-VvZcnTEgPXlAYEf2+2WZ2xlU4TWhTz+dBiA4j76GSvM=";
|
vendorHash = "sha256-VvZcnTEgPXlAYEf2+2WZ2xlU4TWhTz+dBiA4j76GSvM=";
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="description" content="Ny4 IP Checker" />
|
<meta name="description" content="Ny4 IP Checker" />
|
||||||
<title>What is my IP?</title>
|
<title>What is my IP?</title>
|
||||||
<link href="static/generated.css" rel="stylesheet" />
|
<link href="generated.css" rel="stylesheet" />
|
||||||
<script src="static/script.js" defer></script>
|
<script src="script.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class="text-violet-950 bg-violet-100 dark:text-slate-100 dark:bg-slate-900 font-mono font-medium text-sm md:text-base"
|
class="text-violet-950 bg-violet-100 dark:text-slate-100 dark:bg-slate-900 font-mono font-medium text-sm md:text-base"
|
||||||
|
@ -26,8 +26,8 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="table-cell">local</td>
|
<td class="table-cell">local</td>
|
||||||
<td class="table-cell">{{.sourceIP}}</td>
|
<td class="table-cell" id="check-ny4-ip">Loading...</td>
|
||||||
<td class="table-cell">{{.sourceCountry}}</td>
|
<td class="table-cell" id="check-ny4-location">Loading...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="table-cell">ip.sb</td>
|
<td class="table-cell">ip.sb</td>
|
65
ui/script.js
Normal file
65
ui/script.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const [ny4, ipsb, speedtestcn, ipapiis] = await Promise.all([
|
||||||
|
fetchAsync("https://ip.ny4.dev/api/v1/ip"),
|
||||||
|
fetchAsync("https://api.ip.sb/geoip"),
|
||||||
|
fetchAsync("https://api-v3.speedtest.cn/ip"),
|
||||||
|
fetchAsync("https://api.ipapi.is/"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
updateDom("check-ny4-ip", ny4.ip);
|
||||||
|
updateDom("check-ny4-location", [
|
||||||
|
ny4.city,
|
||||||
|
ny4.region,
|
||||||
|
ny4.country,
|
||||||
|
ny4.organization,
|
||||||
|
]);
|
||||||
|
|
||||||
|
updateDom("check-ipsb-ip", ipsb.ip);
|
||||||
|
updateDom("check-ipsb-location", [
|
||||||
|
ipsb.city,
|
||||||
|
ipsb.region,
|
||||||
|
ipsb.country,
|
||||||
|
ipsb.isp,
|
||||||
|
]);
|
||||||
|
|
||||||
|
updateDom("check-speedtestcn-ip", speedtestcn.data.ip);
|
||||||
|
updateDom("check-speedtestcn-location", [
|
||||||
|
speedtestcn.data.city,
|
||||||
|
speedtestcn.data.province,
|
||||||
|
speedtestcn.data.country,
|
||||||
|
speedtestcn.data.isp,
|
||||||
|
]);
|
||||||
|
|
||||||
|
updateDom("check-ipapiis-ip", ipapiis.ip);
|
||||||
|
updateDom("check-ipapiis-location", [
|
||||||
|
ipapiis.location.city,
|
||||||
|
ipapiis.location.state,
|
||||||
|
ipapiis.location.country,
|
||||||
|
ipapiis.company.name,
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error fetching data:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAsync(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Fetch error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDom(elementId, content) {
|
||||||
|
document.getElementById(elementId).innerHTML = Array.isArray(content)
|
||||||
|
? content.join(", ")
|
||||||
|
: content;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = main;
|
Loading…
Reference in a new issue