Files
zzgandClaude Opus 5.5 0edc63fd2f Serve all output types on i.z.gd; redirect i.zk.gd to it
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>
2026-10-21 10:15:13 -07:00

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())
}