diff --git a/README.md b/README.md index 1cef1d6..f9f5b41 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,28 @@ Client --- +## Domain-Lookup (optional) + +Für die Validierung von Institutions-Domains kann ein Lookup genutzt werden: + +``` +GET /lookup?domain=uni-stuttgart.de +``` + +Antwort (JSON): +```json +{ + "domain": "uni-stuttgart.de", + "nren": true, + "asn": 12345, + "asn_org": "Universitaet Stuttgart", + "ips": ["129.69.1.1"], + "matched_ip": "129.69.1.1" +} +``` + +--- + ## Integration Der Service wird als **Traefik ForwardAuth Middleware** eingebunden. diff --git a/README_technical.md b/README_technical.md index 1cef1d6..ff9154d 100644 --- a/README_technical.md +++ b/README_technical.md @@ -60,6 +60,28 @@ Client --- +## Domain-Lookup (optional) + +Für Backend-Validierung von Institutions-Domains: + +``` +GET /lookup?domain=uni-stuttgart.de +``` + +Antwort (JSON): +```json +{ + "domain": "uni-stuttgart.de", + "nren": true, + "asn": 12345, + "asn_org": "Universitaet Stuttgart", + "ips": ["129.69.1.1"], + "matched_ip": "129.69.1.1" +} +``` + +--- + ## Integration Der Service wird als **Traefik ForwardAuth Middleware** eingebunden. diff --git a/main.go b/main.go index 4194708..6df6ef7 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "encoding/json" "log" "net" "net/http" @@ -19,6 +20,16 @@ type asnRecord struct { Org string `maxminddb:"autonomous_system_organization"` } +type lookupResponse struct { + Domain string `json:"domain"` + NREN bool `json:"nren"` + ASN *uint `json:"asn,omitempty"` + ASNOrg string `json:"asn_org,omitempty"` + IPs []string `json:"ips"` + MatchedIP string `json:"matched_ip,omitempty"` + Error string `json:"error,omitempty"` +} + type server struct { db *maxminddb.Reader nrenASNs map[uint]struct{} @@ -76,6 +87,12 @@ func remoteIP(r *http.Request) string { return r.RemoteAddr } +func writeJSON(w http.ResponseWriter, status int, payload any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(payload) +} + func (s *server) authHandler(w http.ResponseWriter, r *http.Request) { if !s.ready.Load() { w.WriteHeader(http.StatusServiceUnavailable) @@ -116,6 +133,77 @@ func (s *server) authHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func (s *server) lookupHandler(w http.ResponseWriter, r *http.Request) { + if !s.ready.Load() { + writeJSON(w, http.StatusServiceUnavailable, lookupResponse{ + NREN: false, + Error: "service not ready", + }) + return + } + + domain := strings.TrimSpace(r.URL.Query().Get("domain")) + if domain == "" { + writeJSON(w, http.StatusBadRequest, lookupResponse{ + NREN: false, + Error: "missing domain", + }) + return + } + + ips, err := net.LookupIP(domain) + if err != nil || len(ips) == 0 { + writeJSON(w, http.StatusOK, lookupResponse{ + Domain: domain, + NREN: false, + Error: "domain lookup failed", + }) + return + } + + resp := lookupResponse{ + Domain: domain, + NREN: false, + IPs: make([]string, 0, len(ips)), + } + + var firstASN *uint + var firstOrg string + + for _, ip := range ips { + ipStr := ip.String() + resp.IPs = append(resp.IPs, ipStr) + + var rec asnRecord + if err := s.db.Lookup(ip, &rec); err != nil || rec.ASN == 0 { + continue + } + + if firstASN == nil { + firstASN = new(uint) + *firstASN = rec.ASN + firstOrg = rec.Org + } + + if _, ok := s.nrenASNs[rec.ASN]; ok { + asn := rec.ASN + resp.NREN = true + resp.ASN = &asn + resp.ASNOrg = rec.Org + resp.MatchedIP = ipStr + writeJSON(w, http.StatusOK, resp) + return + } + } + + if firstASN != nil { + resp.ASN = firstASN + resp.ASNOrg = firstOrg + } + + writeJSON(w, http.StatusOK, resp) +} + func main() { mmdbPath := getenv("MMDB_PATH", "/data/GeoLite2-ASN.mmdb") asnListPath := getenv("ASN_LIST_PATH", "/data/nren_asns.txt") @@ -146,6 +234,7 @@ func main() { mux := http.NewServeMux() mux.HandleFunc("/auth", s.authHandler) + mux.HandleFunc("/lookup", s.lookupHandler) mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { if s.asnCount < s.minASN { w.WriteHeader(http.StatusServiceUnavailable)