feat: add domain lookup endpoint
This commit is contained in:
22
README.md
22
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.
|
||||
|
||||
@@ -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
89
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)
|
||||
|
||||
Reference in New Issue
Block a user