mirror of
https://github.com/therootcompany/golib.git
synced 2026-04-24 12:48:00 +00:00
Positional args are IPs to check and print; at least one IP or --serve must be provided. Refactor server.go: split handle into lookup + writeText methods so main can reuse them. geoip is no longer managed via dataset.Group — it's a single atomic.Pointer[geoip.Databases]. The Fetcher (httpcache or PollFiles) still drives refresh, but via an inline ticker in the serve branch that fetches, reopens, and swaps.
121 lines
3.1 KiB
Go
121 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/therootcompany/golib/net/geoip"
|
|
)
|
|
|
|
// Result is the JSON verdict for a single IP.
|
|
type Result struct {
|
|
IP string `json:"ip"`
|
|
Blocked bool `json:"blocked"`
|
|
BlockedInbound bool `json:"blocked_inbound"`
|
|
BlockedOutbound bool `json:"blocked_outbound"`
|
|
Geo geoip.Info `json:"geo,omitzero"`
|
|
}
|
|
|
|
// lookup builds a Result for ip against the currently loaded blocklists
|
|
// and GeoIP databases.
|
|
func (c *IPCheck) lookup(ip string) Result {
|
|
in := c.inbound.Value().Contains(ip)
|
|
out := c.outbound.Value().Contains(ip)
|
|
return Result{
|
|
IP: ip,
|
|
Blocked: in || out,
|
|
BlockedInbound: in,
|
|
BlockedOutbound: out,
|
|
Geo: c.geo.Load().Lookup(ip),
|
|
}
|
|
}
|
|
|
|
// writeText renders res as human-readable plain text.
|
|
func (c *IPCheck) writeText(w io.Writer, res Result) {
|
|
switch {
|
|
case res.BlockedInbound && res.BlockedOutbound:
|
|
fmt.Fprintf(w, "%s is BLOCKED (inbound + outbound)\n", res.IP)
|
|
case res.BlockedInbound:
|
|
fmt.Fprintf(w, "%s is BLOCKED (inbound)\n", res.IP)
|
|
case res.BlockedOutbound:
|
|
fmt.Fprintf(w, "%s is BLOCKED (outbound)\n", res.IP)
|
|
default:
|
|
fmt.Fprintf(w, "%s is allowed\n", res.IP)
|
|
}
|
|
var parts []string
|
|
if res.Geo.City != "" {
|
|
parts = append(parts, res.Geo.City)
|
|
}
|
|
if res.Geo.Region != "" {
|
|
parts = append(parts, res.Geo.Region)
|
|
}
|
|
if res.Geo.Country != "" {
|
|
parts = append(parts, fmt.Sprintf("%s (%s)", res.Geo.Country, res.Geo.CountryISO))
|
|
}
|
|
if len(parts) > 0 {
|
|
fmt.Fprintf(w, " Location: %s\n", strings.Join(parts, ", "))
|
|
}
|
|
if res.Geo.ASN != 0 {
|
|
fmt.Fprintf(w, " ASN: AS%d %s\n", res.Geo.ASN, res.Geo.ASNOrg)
|
|
}
|
|
}
|
|
|
|
func (c *IPCheck) handle(w http.ResponseWriter, r *http.Request) {
|
|
ip := strings.TrimSpace(r.URL.Query().Get("ip"))
|
|
if ip == "" {
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
first, _, _ := strings.Cut(xff, ",")
|
|
ip = strings.TrimSpace(first)
|
|
} else if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
|
ip = host
|
|
} else {
|
|
ip = r.RemoteAddr
|
|
}
|
|
}
|
|
res := c.lookup(ip)
|
|
|
|
if r.URL.Query().Get("format") == "json" ||
|
|
strings.Contains(r.Header.Get("Accept"), "application/json") {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
_ = enc.Encode(res)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
c.writeText(w, res)
|
|
}
|
|
|
|
func (c *IPCheck) serve(ctx context.Context) error {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /check", c.handle)
|
|
mux.HandleFunc("GET /{$}", c.handle)
|
|
|
|
srv := &http.Server{
|
|
Addr: c.Bind,
|
|
Handler: mux,
|
|
BaseContext: func(_ net.Listener) context.Context { return ctx },
|
|
}
|
|
go func() {
|
|
<-ctx.Done()
|
|
shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = srv.Shutdown(shutCtx)
|
|
}()
|
|
|
|
log.Printf("listening on %s", c.Bind)
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|