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";
|
||||
version = "dev";
|
||||
|
||||
src = ./.;
|
||||
src = ./api;
|
||||
|
||||
vendorHash = "sha256-VvZcnTEgPXlAYEf2+2WZ2xlU4TWhTz+dBiA4j76GSvM=";
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Ny4 IP Checker" />
|
||||
<title>What is my IP?</title>
|
||||
<link href="static/generated.css" rel="stylesheet" />
|
||||
<script src="static/script.js" defer></script>
|
||||
<link href="generated.css" rel="stylesheet" />
|
||||
<script src="script.js" defer></script>
|
||||
</head>
|
||||
<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"
|
||||
|
@ -26,8 +26,8 @@
|
|||
<tbody>
|
||||
<tr>
|
||||
<td class="table-cell">local</td>
|
||||
<td class="table-cell">{{.sourceIP}}</td>
|
||||
<td class="table-cell">{{.sourceCountry}}</td>
|
||||
<td class="table-cell" id="check-ny4-ip">Loading...</td>
|
||||
<td class="table-cell" id="check-ny4-location">Loading...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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