i.z.gd now takes ?output=txt|json|xml|csv|html (?nice kept as an alias for html). i.zk.gd becomes a plain nginx 301 to i.z.gd, so the service no longer routes on Host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
3.9 KiB
Go
149 lines
3.9 KiB
Go
// ipecho serves the "what is my IP" page for i.z.gd,
|
|
// replacing the old PHP scripts (see legacy/). It runs behind nginx,
|
|
// which passes the client address in X-Real-IP.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"flag"
|
|
"html/template"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type info struct {
|
|
IP string `json:"ip"`
|
|
Hostname string `json:"hostname"`
|
|
UserAgent string `json:"user-agent"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
func (i info) fields() []string {
|
|
return []string{i.IP, i.Hostname, i.UserAgent, strconv.FormatInt(i.Timestamp, 10)}
|
|
}
|
|
|
|
var resolver = &net.Resolver{}
|
|
|
|
// clientIP trusts X-Real-IP only when the request comes from loopback (nginx).
|
|
func clientIP(r *http.Request) string {
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
host = r.RemoteAddr
|
|
}
|
|
if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() {
|
|
if real := net.ParseIP(strings.TrimSpace(r.Header.Get("X-Real-IP"))); real != nil {
|
|
return real.String()
|
|
}
|
|
}
|
|
return host
|
|
}
|
|
|
|
// hostname mirrors PHP's gethostbyaddr: the PTR name, or the IP if there is none.
|
|
func hostname(ctx context.Context, ip string) string {
|
|
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
|
defer cancel()
|
|
names, err := resolver.LookupAddr(ctx, ip)
|
|
if err != nil || len(names) == 0 {
|
|
return ip
|
|
}
|
|
return strings.TrimSuffix(names[0], ".")
|
|
}
|
|
|
|
func gather(r *http.Request) info {
|
|
ip := clientIP(r)
|
|
return info{IP: ip, Hostname: hostname(r.Context(), ip), UserAgent: r.UserAgent(), Timestamp: time.Now().Unix()}
|
|
}
|
|
|
|
var niceTmpl = template.Must(template.New("nice").Parse(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
body {
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>{{range $i, $f := .}}{{if $i}}<br><br>{{end}}{{$f}}{{end}}</body>
|
|
</html>`))
|
|
|
|
func writeText(w http.ResponseWriter, i info) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Write([]byte(strings.Join(i.fields(), "\n")))
|
|
}
|
|
|
|
// serve writes the info as ?output=txt|json|xml|csv|html (default txt).
|
|
// ?nice is the old spelling of output=html.
|
|
func serve(w http.ResponseWriter, r *http.Request) {
|
|
i := gather(r)
|
|
q := r.URL.Query()
|
|
out := q.Get("output")
|
|
if _, ok := q["nice"]; ok && out == "" {
|
|
out = "html"
|
|
}
|
|
switch out {
|
|
case "html":
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
niceTmpl.Execute(w, i.fields())
|
|
case "xml":
|
|
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
|
|
var b strings.Builder
|
|
b.WriteString("<?xml version=\"1.0\"?>\n<info>\n")
|
|
for _, kv := range [][2]string{{"ip", i.IP}, {"hostname", i.Hostname}, {"user-agent", i.UserAgent}, {"timestamp", strconv.FormatInt(i.Timestamp, 10)}} {
|
|
b.WriteString("\t<" + kv[0] + ">")
|
|
xml.EscapeText(&b, []byte(kv[1]))
|
|
b.WriteString("</" + kv[0] + ">\n")
|
|
}
|
|
b.WriteString("</info>")
|
|
w.Write([]byte(b.String()))
|
|
case "json":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
b, _ := json.Marshal(i)
|
|
w.Write(b)
|
|
case "csv":
|
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
|
cw := csv.NewWriter(w)
|
|
cw.Write(i.fields())
|
|
cw.Flush()
|
|
default:
|
|
writeText(w, i)
|
|
}
|
|
}
|
|
|
|
func handler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if r.URL.Path != "/" && r.URL.Path != "/index.php" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
serve(w, r)
|
|
})
|
|
}
|
|
|
|
func main() {
|
|
addr := flag.String("addr", "127.0.0.1:8490", "listen address")
|
|
flag.Parse()
|
|
srv := &http.Server{
|
|
Addr: *addr,
|
|
Handler: handler(),
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
MaxHeaderBytes: 16 << 10,
|
|
}
|
|
log.Printf("ipecho listening on %s", *addr)
|
|
log.Fatal(srv.ListenAndServe())
|
|
}
|