feat: add domain lookup endpoint

This commit is contained in:
2026-02-20 15:53:55 +01:00
parent d355e7e6a9
commit adf290e4ac
3 changed files with 133 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.

89
main.go
View File

@@ -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)