From b4214b63d7c73cb0fec55b4e678a98327a159d48 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sat, 4 Jan 2020 00:28:25 +0000 Subject: Refactor out of main a bit --- bind/config.go | 19 +++++++ go.mod | 1 + go.sum | 5 ++ main.go | 177 +++++++++++++++++++++++++++++---------------------------- web.go | 26 +++++++++ 5 files changed, 141 insertions(+), 87 deletions(-) create mode 100644 web.go diff --git a/bind/config.go b/bind/config.go index 584b1ee..42b97cf 100644 --- a/bind/config.go +++ b/bind/config.go @@ -54,6 +54,25 @@ func (c *BINDConfig) Zone(view, name string) *Zone { return z } +// Find the closest zone that we manage by striping dotted components off the +// front of the domain until one matches. If there is a match return the zone +// that matched and any prefix components, if any, as a dotted string. If none +// match then return nil. +func (c *BINDConfig) FindClosestZone(zoneIn, view string) (*Zone, string) { + suffix := "" + prefix := []string{} + + zc := strings.Split(zoneIn, ".") + for i := 0; i <= len(zc)-2; i++ { + prefix, suffix = zc[:i], strings.Join(zc[i:], ".") + if zone := c.Zone(view, suffix); zone != nil { + return zone, strings.Join(prefix, ".") + } + } + + return nil, "" +} + func (c *BINDConfig) AddKey(k *Key) { c.keys[k.Name] = k } diff --git a/go.mod b/go.mod index 1fd977c..b8f5134 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.13 require ( github.com/gin-gonic/gin v1.5.0 github.com/miekg/dns v1.1.26 + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect ) diff --git a/go.sum b/go.sum index 1f2c074..2f5ba2a 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -20,9 +21,11 @@ github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -44,6 +47,8 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPT golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index 269b11e..ac9b849 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,10 @@ package main import ( "encoding/base64" "encoding/json" - "fmt" "io/ioutil" "log" "net" "net/http" - "net/url" "regexp" "strings" @@ -18,41 +16,58 @@ import ( ) const ( - ACME_AUTH_KEY = "ACMEAuthUserID" - DDNS_AUTH_KEY = "DDNSAuthZone" + ACME_AUTH_KEY = "ACMEAuthUserID" + DDNS_AUTH_KEY = "DDNSAuthZone" + CTX_SERVER_CONFIG = "ServerConfig" ) var ( - cfg *bind.BINDConfig - secrets *Secrets ipRegexp = regexp.MustCompile(`(?:\[([0-9a-f:]+)\]|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})):\d+`) ) -func init() { - var err error - cfg, err = bind.ParseBINDConfig("zones.conf") +type ServerConfig struct { + BindConfig *bind.BINDConfig + DNSClient *dns.DNSClient + ddnsSecrets map[string]string `json:"DDNS"` + acmeSecrets map[string]map[string]int `json:"ACME"` +} + +func LoadServerConfig(zonesFile, secretsFile string) (*ServerConfig, error) { + scfg := &ServerConfig{ + // TODO: Remove + DNSClient: &dns.DNSClient{Server: "172.16.18.52:53"}, + } + + cfg, err := bind.ParseBINDConfig(zonesFile) if err != nil { - panic(err) + return nil, err } + scfg.BindConfig = cfg - fd, err := ioutil.ReadFile("secrets.json") + fd, err := ioutil.ReadFile(secretsFile) if err != nil { - panic(err) + return nil, err } - secrets = &Secrets{} - if err = json.Unmarshal(fd, secrets); err != nil { - panic(err) + if err = json.Unmarshal(fd, scfg); err != nil { + return nil, err } + + return scfg, nil +} + +func (s *ServerConfig) GetDDNZoneName(k string) string { + v, _ := s.ddnsSecrets[k] + return v } -type Secrets struct { - DDNS map[string]string - ACME map[string]map[string]int +func (s *ServerConfig) AcmeSecretExists(k string) bool { + _, ok := s.acmeSecrets[k] + return ok } -func (s *Secrets) IsACMEClientAllowed(key, zone string) bool { - u, ok := s.ACME[key] +func (s *ServerConfig) IsAcmeClientAllowed(key, zone string) bool { + u, ok := s.acmeSecrets[key] if !ok { return false } @@ -74,12 +89,12 @@ type DDNSUpdateRequest struct { Key string `form:"key" binding:"required"` } -type ACMEChallenge struct { +type AcmeChallenge struct { Zone string `json:"zone" binding:"required"` Challenge string `json:"challenge" binding:"required"` } -type ACMEChallengeID struct { +type AcmeChallengeID struct { Zone string Prefix string Challenge string @@ -95,49 +110,10 @@ func joinDomainParts(parts ...string) string { return strings.Join(p, ".") } -// Find the closest zone that we manage by striping dotted components off the -// front of the domain until one matches. If there is a match return the zone -// that matched and any prefix components, if any, as a dotted string. If none -// match then return nil. -func findClosestZone(cfg *bind.BINDConfig, zoneIn, view string) (*bind.Zone, string) { - suffix := "" - prefix := []string{} - - zc := strings.Split(zoneIn, ".") - for i := 0; i <= len(zc)-2; i++ { - prefix, suffix = zc[:i], strings.Join(zc[i:], ".") - if zone := cfg.Zone(view, suffix); zone != nil { - return zone, strings.Join(prefix, ".") - } - } - - return nil, "" -} - -func makeURL(r *http.Request, path string, subs ...interface{}) *url.URL { - scheme := "https" - - if r.TLS == nil { - scheme = "http" - } - - // Always defer to whatever the proxy told us it was doing because this - // could be a mullet-VIP in either direction. - if fwProto := r.Header.Get("X-Forwarded-Proto"); fwProto != "" { - scheme = fwProto - } - - return &url.URL{ - Scheme: scheme, - Host: r.Host, - Path: fmt.Sprintf(path, subs...), - } -} - func createAcmeChallenge(c *gin.Context) { - dc := dns.DNSClient{Server: "172.16.18.52:53"} + cfg := GetServerConfig(c) - var ch ACMEChallenge + var ch AcmeChallenge if err := c.ShouldBindJSON(&ch); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), @@ -145,7 +121,7 @@ func createAcmeChallenge(c *gin.Context) { return } - zone, prefix := findClosestZone(cfg, ch.Zone, "external") + zone, prefix := cfg.BindConfig.FindClosestZone(ch.Zone, "external") if zone == nil { c.JSON(http.StatusNotFound, gin.H{ "error": "Zone not found", @@ -153,7 +129,7 @@ func createAcmeChallenge(c *gin.Context) { return } - if v := c.GetString(ACME_AUTH_KEY); !secrets.IsACMEClientAllowed(v, zone.Name) { + if v := c.GetString(ACME_AUTH_KEY); !cfg.IsAcmeClientAllowed(v, zone.Name) { c.JSON(http.StatusForbidden, gin.H{ "error": "Zone update not allowed", }) @@ -161,7 +137,7 @@ func createAcmeChallenge(c *gin.Context) { } // Do this first, in-case it fails (even though it should never fail) - id, err := json.Marshal(ACMEChallengeID{ + id, err := json.Marshal(AcmeChallengeID{ Zone: zone.Name, Prefix: prefix, Challenge: ch.Challenge, @@ -183,7 +159,7 @@ func createAcmeChallenge(c *gin.Context) { } // Cleanup any old challenges before adding a new one - if err := dc.RemoveAll(zone, t); err != nil { + if err := cfg.DNSClient.RemoveAll(zone, t); err != nil { log.Printf("error RemoveAll: %s", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), @@ -191,7 +167,7 @@ func createAcmeChallenge(c *gin.Context) { return } - if err := dc.Insert(zone, t); err != nil { + if err := cfg.DNSClient.Insert(zone, t); err != nil { log.Printf("error Insert: %s", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), @@ -206,7 +182,7 @@ func createAcmeChallenge(c *gin.Context) { } func deleteAcmeChallenge(c *gin.Context) { - dc := dns.DNSClient{Server: "172.16.18.52:53"} + cfg := GetServerConfig(c) rid, err := base64.URLEncoding.DecodeString(c.Param("id")) if err != nil { @@ -216,7 +192,7 @@ func deleteAcmeChallenge(c *gin.Context) { return } - var id ACMEChallengeID + var id AcmeChallengeID if err = json.Unmarshal(rid, &id); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "unable to decode ID", @@ -224,7 +200,7 @@ func deleteAcmeChallenge(c *gin.Context) { return } - zone := cfg.Zone("external", id.Zone) + zone := cfg.BindConfig.Zone("external", id.Zone) if zone == nil { c.JSON(http.StatusNotFound, gin.H{ "error": "Zone not found", @@ -232,7 +208,7 @@ func deleteAcmeChallenge(c *gin.Context) { return } - if v := c.GetString(ACME_AUTH_KEY); !secrets.IsACMEClientAllowed(v, zone.Name) { + if v := c.GetString(ACME_AUTH_KEY); !cfg.IsAcmeClientAllowed(v, zone.Name) { c.JSON(http.StatusForbidden, gin.H{ "error": "Zone update not allowed", }) @@ -245,7 +221,7 @@ func deleteAcmeChallenge(c *gin.Context) { Txt: []string{id.Challenge}, } - if err := dc.Remove(zone, t); err != nil { + if err := cfg.DNSClient.Remove(zone, t); err != nil { log.Printf("error Remove: %s", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), @@ -257,7 +233,7 @@ func deleteAcmeChallenge(c *gin.Context) { } func updateDynamicDNS(c *gin.Context) { - dc := dns.DNSClient{Server: "172.16.18.52:53"} + cfg := GetServerConfig(c) res := c.GetString(DDNS_AUTH_KEY) if res == "" { @@ -266,7 +242,7 @@ func updateDynamicDNS(c *gin.Context) { return } - zone, part := findClosestZone(cfg, res, "external") + zone, part := cfg.BindConfig.FindClosestZone(res, "external") if zone == nil { log.Println("ddns: Unable to locate zone") c.AbortWithStatus(http.StatusNotFound) @@ -292,13 +268,13 @@ func updateDynamicDNS(c *gin.Context) { } // Cleanup any old records before adding the new one - if err := dc.RemoveAll(zone, t); err != nil { + if err := cfg.DNSClient.RemoveAll(zone, t); err != nil { log.Printf("ddns RemoveAll: %s", err) c.AbortWithStatus(http.StatusInternalServerError) return } - if err := dc.Insert(zone, t); err != nil { + if err := cfg.DNSClient.Insert(zone, t); err != nil { log.Printf("ddns Insert: %s", err) c.AbortWithStatus(http.StatusInternalServerError) return @@ -330,15 +306,17 @@ func reflectIP(c *gin.Context) { } } -func acmeAuth(c *gin.Context) { +func acmeAuthMiddleware(c *gin.Context) { + cfg := GetServerConfig(c) + _, pwd, ok := c.Request.BasicAuth() if !ok { - c.Request.Header["WWW-Authenticate"] = []string{`Basic realm="closed site"`} + c.Request.Header.Set("WWW-Authenticate", `Basic realm="closed site"`) c.AbortWithStatus(http.StatusUnauthorized) return } - if _, ok := secrets.ACME[pwd]; !ok { + if !cfg.AcmeSecretExists(pwd) { c.AbortWithStatus(http.StatusForbidden) return } else { @@ -348,17 +326,19 @@ func acmeAuth(c *gin.Context) { c.Next() } -func ddnsAuth(c *gin.Context) { +func ddnsAuthMiddleware(c *gin.Context) { + cfg := GetServerConfig(c) + var req DDNSUpdateRequest if err := c.ShouldBind(&req); err != nil { - log.Println("ddnsAuth: No key in request") + log.Println("ddnsAuthMiddleware: No key in request") c.AbortWithStatus(http.StatusNotFound) return } - res, ok := secrets.DDNS[req.Key] - if !ok { - log.Println("ddnsAuth: Unknown secret") + res := cfg.GetDDNZoneName(req.Key) + if res == "" { + log.Println("ddnsAuthMiddleware: Unknown secret") c.AbortWithStatus(http.StatusNotFound) return } else { @@ -368,21 +348,44 @@ func ddnsAuth(c *gin.Context) { c.Next() } +func ConfigContextMiddleware(cfg *ServerConfig) func(*gin.Context) { + return func(c *gin.Context) { + c.Set(CTX_SERVER_CONFIG, cfg) + c.Next() + } +} + +func GetServerConfig(c *gin.Context) *ServerConfig { + v, ok := c.Get(CTX_SERVER_CONFIG) + if !ok { + // This should never happen if the config context middlware is in place + panic("Unable to get config from request") + } + + return v.(*ServerConfig) +} + func main() { gin.SetMode(gin.DebugMode) + cfg, err := LoadServerConfig("cfg/zones.conf", "cfg/secrets.json") + if err != nil { + panic(err) + } + router := gin.Default() + router.Use(ConfigContextMiddleware(cfg)) router.GET("/reflect-ip", reflectIP) ddns := router.Group("/dynamic-dns") - ddns.Use(ddnsAuth) + ddns.Use(ddnsAuthMiddleware) { ddns.POST("", updateDynamicDNS) } acme := router.Group("/acme") - acme.Use(acmeAuth) + acme.Use(acmeAuthMiddleware) { acme.POST("", createAcmeChallenge) acme.DELETE("/:id", deleteAcmeChallenge) diff --git a/web.go b/web.go new file mode 100644 index 0000000..b64c1de --- /dev/null +++ b/web.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "net/http" + "net/url" +) + +func makeURL(r *http.Request, path string, subs ...interface{}) *url.URL { + scheme := "https" + if r.TLS == nil { + scheme = "http" + } + + // Always defer to whatever the proxy told us it was doing because this + // could be a mullet-VIP in either direction. + if fwProto := r.Header.Get("X-Forwarded-Proto"); fwProto != "" { + scheme = fwProto + } + + return &url.URL{ + Scheme: scheme, + Host: r.Host, + Path: fmt.Sprintf(path, subs...), + } +} -- cgit v1.2.3