summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2020-01-04 03:59:53 +0000
committerMike Crute <mike@crute.us>2020-01-04 03:59:53 +0000
commit8ed9671c0b4f78711858448cf3b4ee9af0eba51e (patch)
tree2b6e9b9f1958101de8501a36fc60124b86a35c6f
parentb4214b63d7c73cb0fec55b4e678a98327a159d48 (diff)
downloadgo_ddns_manager-8ed9671c0b4f78711858448cf3b4ee9af0eba51e.tar.bz2
go_ddns_manager-8ed9671c0b4f78711858448cf3b4ee9af0eba51e.tar.xz
go_ddns_manager-8ed9671c0b4f78711858448cf3b4ee9af0eba51e.zip
Refactor into an application
-rw-r--r--dns/cilent.go78
-rw-r--r--go.mod1
-rw-r--r--go.sum33
-rw-r--r--main.go429
-rw-r--r--web.go26
-rw-r--r--web/config.go75
-rw-r--r--web/controllers/acme.go149
-rw-r--r--web/controllers/ddns.go51
-rw-r--r--web/controllers/reflect_ip.go19
-rw-r--r--web/middleware/acme.go33
-rw-r--r--web/middleware/config_context.go26
-rw-r--r--web/middleware/ddns.go40
-rw-r--r--web/utils.go117
13 files changed, 653 insertions, 424 deletions
diff --git a/dns/cilent.go b/dns/cilent.go
index 63e3734..989bb4d 100644
--- a/dns/cilent.go
+++ b/dns/cilent.go
@@ -1,17 +1,44 @@
1package dns 1package dns
2 2
3import ( 3import (
4 "code.crute.me/mcrute/go_ddns_manager/bind"
5 "github.com/miekg/dns" 4 "github.com/miekg/dns"
5
6 "code.crute.me/mcrute/go_ddns_manager/bind"
6) 7)
7 8
8type DNSClient struct { 9type DNSClient struct {
9 Server string 10 Server string
10} 11}
11 12
13type DNSTransaction struct {
14 zone *bind.Zone
15 key *bind.Key
16 msg *dns.Msg
17}
18
19func (t *DNSTransaction) Upsert(rrs ...RR) *DNSTransaction {
20 t.RemoveAll(rrs...).Insert(rrs...)
21 return t
22}
23
24func (t *DNSTransaction) Insert(rrs ...RR) *DNSTransaction {
25 t.msg.Insert(toRRSet(t.zone, rrs...))
26 return t
27}
28
29func (t *DNSTransaction) Remove(rrs ...RR) *DNSTransaction {
30 t.msg.Remove(toRRSet(t.zone, rrs...))
31 return t
32}
33
34func (t *DNSTransaction) RemoveAll(rrs ...RR) *DNSTransaction {
35 t.msg.RemoveRRset(toRRSet(t.zone, rrs...))
36 return t
37}
38
12func (c *DNSClient) AXFR(zone *bind.Zone) (chan *dns.Envelope, error) { 39func (c *DNSClient) AXFR(zone *bind.Zone) (chan *dns.Envelope, error) {
13 k := zone.Keys()[0] 40 k := zone.Keys()[0]
14 t := &dns.Transfer{TsigSecret: k.AsMap()} 41 t := &dns.Transfer{TsigSecret: k.AsMap()} // Always uses tcp
15 42
16 m := &dns.Msg{} 43 m := &dns.Msg{}
17 m.SetAxfr(zone.Name) 44 m.SetAxfr(zone.Name)
@@ -20,48 +47,31 @@ func (c *DNSClient) AXFR(zone *bind.Zone) (chan *dns.Envelope, error) {
20 return t.In(m, c.Server) 47 return t.In(m, c.Server)
21} 48}
22 49
23func (c *DNSClient) Insert(zone *bind.Zone, rrs ...RR) error { 50func (c *DNSClient) StartUpdate(zone *bind.Zone) *DNSTransaction {
24 k := zone.Keys()[0]
25 dc := &dns.Client{TsigSecret: k.AsMap()}
26
27 m := &dns.Msg{} 51 m := &dns.Msg{}
28 m.SetUpdate(zone.Name) 52 m.SetUpdate(zone.Name)
29 m.Insert(toRRSet(zone, rrs...))
30 k.Sign(m)
31 53
32 if _, _, err := dc.Exchange(m, c.Server); err != nil { 54 return &DNSTransaction{
33 return err 55 zone: zone,
56 key: zone.Keys()[0],
57 msg: m,
34 } 58 }
35
36 return nil
37} 59}
38 60
39func (c *DNSClient) Remove(zone *bind.Zone, rrs ...RR) error { 61func (c *DNSClient) SendUpdate(t *DNSTransaction) error {
40 k := zone.Keys()[0] 62 udp := &dns.Client{Net: "udp", TsigSecret: t.key.AsMap()}
41 dc := &dns.Client{TsigSecret: k.AsMap()} 63 tcp := &dns.Client{Net: "tcp", TsigSecret: t.key.AsMap()}
42 64
43 m := &dns.Msg{} 65 t.msg.SetEdns0(4096, false)
44 m.SetUpdate(zone.Name) 66 t.key.Sign(t.msg)
45 m.Remove(toRRSet(zone, rrs...))
46 k.Sign(m)
47 67
48 if _, _, err := dc.Exchange(m, c.Server); err != nil { 68 in, _, err := udp.Exchange(t.msg, c.Server)
49 return err 69 if in != nil && in.Truncated {
70 // If the TCP request succeeds, the err will reset to nil
71 in, _, err = tcp.Exchange(t.msg, c.Server)
50 } 72 }
51 73
52 return nil 74 if err != nil {
53}
54
55func (c *DNSClient) RemoveAll(zone *bind.Zone, rrs ...RR) error {
56 k := zone.Keys()[0]
57 dc := &dns.Client{TsigSecret: k.AsMap()}
58
59 m := &dns.Msg{}
60 m.SetUpdate(zone.Name)
61 m.RemoveRRset(toRRSet(zone, rrs...))
62 k.Sign(m)
63
64 if _, _, err := dc.Exchange(m, c.Server); err != nil {
65 return err 75 return err
66 } 76 }
67 77
diff --git a/go.mod b/go.mod
index b8f5134..53f7ef2 100644
--- a/go.mod
+++ b/go.mod
@@ -5,5 +5,6 @@ go 1.13
5require ( 5require (
6 github.com/gin-gonic/gin v1.5.0 6 github.com/gin-gonic/gin v1.5.0
7 github.com/miekg/dns v1.1.26 7 github.com/miekg/dns v1.1.26
8 github.com/spf13/cobra v0.0.5
8 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect 9 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
9) 10)
diff --git a/go.sum b/go.sum
index 2f5ba2a..a6a4091 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,13 @@
1github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
3github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
4github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
5github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
6github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
1github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
4github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 11github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
5github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 12github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
6github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= 13github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
@@ -12,25 +19,47 @@ github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEK
12github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 19github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
13github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
14github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 21github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
22github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
23github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
24github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
15github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 25github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
16github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 26github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
17github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 27github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
28github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
18github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 29github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
19github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 30github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
20github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= 31github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
21github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= 32github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
33github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
34github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
35github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
22github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 36github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
37github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
23github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 38github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
39github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
24github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 40github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
25github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
42github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
43github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
44github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
45github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
46github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
47github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
48github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
49github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
50github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
26github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
52github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
27github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 53github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
28github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 54github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
29github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 55github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
30github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 56github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
31github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 57github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
58github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
32github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 59github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
33github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 60github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
61github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
62golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
34golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 63golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
35golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= 64golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
36golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= 65golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
@@ -40,6 +69,7 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7
40golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 69golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
41golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 70golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
42golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
72golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
43golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
44golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -47,12 +77,13 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPT
47golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
48golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 78golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
49golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 79golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
50golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
51golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 80golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
52golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 81golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
53golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 82golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
54golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
84gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
55gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 85gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
86gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
56gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 87gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
57gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= 88gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
58gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 89gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
diff --git a/main.go b/main.go
index ac9b849..367814d 100644
--- a/main.go
+++ b/main.go
@@ -1,395 +1,98 @@
1package main 1package main
2 2
3import ( 3import (
4 "encoding/base64" 4 "fmt"
5 "encoding/json"
6 "io/ioutil"
7 "log" 5 "log"
8 "net" 6 "os"
9 "net/http" 7 "strconv"
10 "regexp"
11 "strings"
12 8
13 "code.crute.me/mcrute/go_ddns_manager/bind"
14 "code.crute.me/mcrute/go_ddns_manager/dns"
15 "github.com/gin-gonic/gin" 9 "github.com/gin-gonic/gin"
16) 10 "github.com/spf13/cobra"
17
18const (
19 ACME_AUTH_KEY = "ACMEAuthUserID"
20 DDNS_AUTH_KEY = "DDNSAuthZone"
21 CTX_SERVER_CONFIG = "ServerConfig"
22)
23 11
24var ( 12 "code.crute.me/mcrute/go_ddns_manager/web"
25 ipRegexp = regexp.MustCompile(`(?:\[([0-9a-f:]+)\]|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})):\d+`) 13 "code.crute.me/mcrute/go_ddns_manager/web/controllers"
14 "code.crute.me/mcrute/go_ddns_manager/web/middleware"
26) 15)
27 16
28type ServerConfig struct { 17// TODO: use from a common package
29 BindConfig *bind.BINDConfig 18func MustGetString(c *cobra.Command, k string) string {
30 DNSClient *dns.DNSClient 19 f := c.Flags().Lookup(k)
31 ddnsSecrets map[string]string `json:"DDNS"` 20 if f == nil {
32 acmeSecrets map[string]map[string]int `json:"ACME"` 21 panic(fmt.Errorf("No flag named %s", k))
33}
34
35func LoadServerConfig(zonesFile, secretsFile string) (*ServerConfig, error) {
36 scfg := &ServerConfig{
37 // TODO: Remove
38 DNSClient: &dns.DNSClient{Server: "172.16.18.52:53"},
39 }
40
41 cfg, err := bind.ParseBINDConfig(zonesFile)
42 if err != nil {
43 return nil, err
44 } 22 }
45 scfg.BindConfig = cfg
46
47 fd, err := ioutil.ReadFile(secretsFile)
48 if err != nil {
49 return nil, err
50 }
51
52 if err = json.Unmarshal(fd, scfg); err != nil {
53 return nil, err
54 }
55
56 return scfg, nil
57}
58 23
59func (s *ServerConfig) GetDDNZoneName(k string) string { 24 return f.Value.String()
60 v, _ := s.ddnsSecrets[k]
61 return v
62} 25}
63 26
64func (s *ServerConfig) AcmeSecretExists(k string) bool { 27// TODO: use from a common package
65 _, ok := s.acmeSecrets[k] 28func MustGetBool(c *cobra.Command, k string) bool {
66 return ok 29 f := c.Flags().Lookup(k)
67} 30 if f == nil {
68 31 panic(fmt.Errorf("No flag named %s", k))
69func (s *ServerConfig) IsAcmeClientAllowed(key, zone string) bool {
70 u, ok := s.acmeSecrets[key]
71 if !ok {
72 return false
73 }
74
75 p, ok := u[zone]
76 if ok && p == 1 {
77 return true
78 } 32 }
79 33
80 p, ok = u[strings.TrimRight(zone, ".")] 34 t, err := strconv.ParseBool(f.Value.String())
81 if ok && p == 1 {
82 return true
83 }
84
85 return false
86}
87
88type DDNSUpdateRequest struct {
89 Key string `form:"key" binding:"required"`
90}
91
92type AcmeChallenge struct {
93 Zone string `json:"zone" binding:"required"`
94 Challenge string `json:"challenge" binding:"required"`
95}
96
97type AcmeChallengeID struct {
98 Zone string
99 Prefix string
100 Challenge string
101}
102
103func joinDomainParts(parts ...string) string {
104 p := []string{}
105 for _, i := range parts {
106 if strings.TrimSpace(i) != "" {
107 p = append(p, i)
108 }
109 }
110 return strings.Join(p, ".")
111}
112
113func createAcmeChallenge(c *gin.Context) {
114 cfg := GetServerConfig(c)
115
116 var ch AcmeChallenge
117 if err := c.ShouldBindJSON(&ch); err != nil {
118 c.JSON(http.StatusBadRequest, gin.H{
119 "error": err.Error(),
120 })
121 return
122 }
123
124 zone, prefix := cfg.BindConfig.FindClosestZone(ch.Zone, "external")
125 if zone == nil {
126 c.JSON(http.StatusNotFound, gin.H{
127 "error": "Zone not found",
128 })
129 return
130 }
131
132 if v := c.GetString(ACME_AUTH_KEY); !cfg.IsAcmeClientAllowed(v, zone.Name) {
133 c.JSON(http.StatusForbidden, gin.H{
134 "error": "Zone update not allowed",
135 })
136 return
137 }
138
139 // Do this first, in-case it fails (even though it should never fail)
140 id, err := json.Marshal(AcmeChallengeID{
141 Zone: zone.Name,
142 Prefix: prefix,
143 Challenge: ch.Challenge,
144 })
145 if err != nil { 35 if err != nil {
146 log.Printf("error: %s", err) 36 panic(err)
147 c.JSON(http.StatusInternalServerError, gin.H{
148 "error": "error encoding ID",
149 })
150 return
151 }
152
153 url := makeURL(c.Request, "/acme/%s", base64.URLEncoding.EncodeToString(id))
154
155 t := &dns.TXT{
156 Name: joinDomainParts("_acme-challenge", prefix),
157 Ttl: 5,
158 Txt: []string{ch.Challenge},
159 }
160
161 // Cleanup any old challenges before adding a new one
162 if err := cfg.DNSClient.RemoveAll(zone, t); err != nil {
163 log.Printf("error RemoveAll: %s", err)
164 c.JSON(http.StatusInternalServerError, gin.H{
165 "error": err.Error(),
166 })
167 return
168 }
169
170 if err := cfg.DNSClient.Insert(zone, t); err != nil {
171 log.Printf("error Insert: %s", err)
172 c.JSON(http.StatusInternalServerError, gin.H{
173 "error": err.Error(),
174 })
175 return
176 }
177
178 c.Writer.Header()["Location"] = []string{url.String()}
179 c.JSON(http.StatusCreated, gin.H{
180 "created": url.String(),
181 })
182}
183
184func deleteAcmeChallenge(c *gin.Context) {
185 cfg := GetServerConfig(c)
186
187 rid, err := base64.URLEncoding.DecodeString(c.Param("id"))
188 if err != nil {
189 c.JSON(http.StatusBadRequest, gin.H{
190 "error": "unable to decode ID",
191 })
192 return
193 }
194
195 var id AcmeChallengeID
196 if err = json.Unmarshal(rid, &id); err != nil {
197 c.JSON(http.StatusBadRequest, gin.H{
198 "error": "unable to decode ID",
199 })
200 return
201 }
202
203 zone := cfg.BindConfig.Zone("external", id.Zone)
204 if zone == nil {
205 c.JSON(http.StatusNotFound, gin.H{
206 "error": "Zone not found",
207 })
208 return
209 }
210
211 if v := c.GetString(ACME_AUTH_KEY); !cfg.IsAcmeClientAllowed(v, zone.Name) {
212 c.JSON(http.StatusForbidden, gin.H{
213 "error": "Zone update not allowed",
214 })
215 return
216 }
217
218 t := &dns.TXT{
219 Name: joinDomainParts("_acme-challenge", id.Prefix),
220 Ttl: 5,
221 Txt: []string{id.Challenge},
222 }
223
224 if err := cfg.DNSClient.Remove(zone, t); err != nil {
225 log.Printf("error Remove: %s", err)
226 c.JSON(http.StatusInternalServerError, gin.H{
227 "error": err.Error(),
228 })
229 return
230 }
231
232 c.JSON(http.StatusNoContent, gin.H{})
233}
234
235func updateDynamicDNS(c *gin.Context) {
236 cfg := GetServerConfig(c)
237
238 res := c.GetString(DDNS_AUTH_KEY)
239 if res == "" {
240 log.Println("ddns: Unable to get auth key")
241 c.AbortWithStatus(http.StatusForbidden)
242 return
243 }
244
245 zone, part := cfg.BindConfig.FindClosestZone(res, "external")
246 if zone == nil {
247 log.Println("ddns: Unable to locate zone")
248 c.AbortWithStatus(http.StatusNotFound)
249 return
250 }
251
252 inip := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0])
253 xff := c.Request.Header.Get("X-Forwarded-For")
254 if xff != "" {
255 inip = net.ParseIP(xff)
256 }
257
258 if inip == nil {
259 log.Println("ddns: Unable to parse IP")
260 c.AbortWithStatus(http.StatusInternalServerError)
261 return
262 }
263
264 t := &dns.A{
265 Name: part,
266 Ttl: 60,
267 A: inip,
268 }
269
270 // Cleanup any old records before adding the new one
271 if err := cfg.DNSClient.RemoveAll(zone, t); err != nil {
272 log.Printf("ddns RemoveAll: %s", err)
273 c.AbortWithStatus(http.StatusInternalServerError)
274 return
275 }
276
277 if err := cfg.DNSClient.Insert(zone, t); err != nil {
278 log.Printf("ddns Insert: %s", err)
279 c.AbortWithStatus(http.StatusInternalServerError)
280 return
281 }
282
283 c.String(http.StatusAccepted, "")
284}
285
286func reflectIP(c *gin.Context) {
287 myIp := c.Request.RemoteAddr
288 xff := c.Request.Header.Get("X-Forwarded-For")
289 if xff != "" {
290 myIp = xff
291 }
292
293 ips := ipRegexp.FindStringSubmatch(myIp)
294 if ips == nil {
295 c.AbortWithStatus(http.StatusInternalServerError)
296 return
297 }
298
299 v6, v4 := ips[1], ips[2]
300 if v6 != "" {
301 c.String(http.StatusOK, v6)
302 } else if v4 != "" {
303 c.String(http.StatusOK, v4)
304 } else {
305 c.AbortWithStatus(http.StatusInternalServerError)
306 }
307}
308
309func acmeAuthMiddleware(c *gin.Context) {
310 cfg := GetServerConfig(c)
311
312 _, pwd, ok := c.Request.BasicAuth()
313 if !ok {
314 c.Request.Header.Set("WWW-Authenticate", `Basic realm="closed site"`)
315 c.AbortWithStatus(http.StatusUnauthorized)
316 return
317 }
318
319 if !cfg.AcmeSecretExists(pwd) {
320 c.AbortWithStatus(http.StatusForbidden)
321 return
322 } else {
323 c.Set(ACME_AUTH_KEY, pwd)
324 }
325
326 c.Next()
327}
328
329func ddnsAuthMiddleware(c *gin.Context) {
330 cfg := GetServerConfig(c)
331
332 var req DDNSUpdateRequest
333 if err := c.ShouldBind(&req); err != nil {
334 log.Println("ddnsAuthMiddleware: No key in request")
335 c.AbortWithStatus(http.StatusNotFound)
336 return
337 }
338
339 res := cfg.GetDDNZoneName(req.Key)
340 if res == "" {
341 log.Println("ddnsAuthMiddleware: Unknown secret")
342 c.AbortWithStatus(http.StatusNotFound)
343 return
344 } else {
345 c.Set(DDNS_AUTH_KEY, res)
346 }
347
348 c.Next()
349}
350
351func ConfigContextMiddleware(cfg *ServerConfig) func(*gin.Context) {
352 return func(c *gin.Context) {
353 c.Set(CTX_SERVER_CONFIG, cfg)
354 c.Next()
355 }
356}
357
358func GetServerConfig(c *gin.Context) *ServerConfig {
359 v, ok := c.Get(CTX_SERVER_CONFIG)
360 if !ok {
361 // This should never happen if the config context middlware is in place
362 panic("Unable to get config from request")
363 } 37 }
364 38
365 return v.(*ServerConfig) 39 return t
366} 40}
367 41
368func main() { 42func makeServer(cfg *web.ServerConfig) *gin.Engine {
369 gin.SetMode(gin.DebugMode)
370
371 cfg, err := LoadServerConfig("cfg/zones.conf", "cfg/secrets.json")
372 if err != nil {
373 panic(err)
374 }
375
376 router := gin.Default() 43 router := gin.Default()
377 router.Use(ConfigContextMiddleware(cfg)) 44 router.Use(middleware.ConfigContextMiddleware(cfg))
378 45
379 router.GET("/reflect-ip", reflectIP) 46 router.GET("/reflect-ip", controllers.ReflectIP)
380 47
381 ddns := router.Group("/dynamic-dns") 48 ddns := router.Group("/dynamic-dns")
382 ddns.Use(ddnsAuthMiddleware) 49 ddns.Use(middleware.DDNSAuthMiddleware)
383 { 50 {
384 ddns.POST("", updateDynamicDNS) 51 ddns.POST("", controllers.UpdateDynamicDNS)
385 } 52 }
386 53
387 acme := router.Group("/acme") 54 acme := router.Group("/acme")
388 acme.Use(acmeAuthMiddleware) 55 acme.Use(middleware.AcmeAuthMiddleware)
389 { 56 {
390 acme.POST("", createAcmeChallenge) 57 acme.POST("", controllers.CreateAcmeChallenge)
391 acme.DELETE("/:id", deleteAcmeChallenge) 58 acme.DELETE("/:id", controllers.DeleteAcmeChallenge)
392 } 59 }
393 60
394 router.Run() 61 return router
62}
63
64func main() {
65 cmd := &cobra.Command{
66 Use: "dns-manage-service [flags] dns-server-ip-port",
67 Short: "DNS manager service",
68 Args: cobra.MinimumNArgs(1),
69 Run: func(cmd *cobra.Command, args []string) {
70 cfg, err := web.LoadServerConfig(
71 MustGetString(cmd, "zones-config"),
72 MustGetString(cmd, "auth-config"),
73 MustGetString(cmd, "view-name"),
74 args[0],
75 )
76 if err != nil {
77 log.Println(err)
78 os.Exit(1)
79 }
80
81 web.GinRun(
82 makeServer(cfg),
83 MustGetBool(cmd, "debug"),
84 MustGetString(cmd, "listen"))
85 },
86 }
87
88 cmd.Flags().StringP("zones-config", "z", "cfg/zones.conf", "Bind key and zones config file")
89 cmd.Flags().StringP("auth-config", "a", "cfg/server_auth.json", "Server auth configuration file")
90 cmd.Flags().StringP("view-name", "n", "external", "Name of view to update for ACME and DDNS")
91 cmd.Flags().StringP("listen", "l", ":9090", "Listen address and port")
92 cmd.Flags().BoolP("debug", "d", false, "Run server in debug mode with debug logs")
93
94 if err := cmd.Execute(); err != nil {
95 log.Println(err)
96 os.Exit(1)
97 }
395} 98}
diff --git a/web.go b/web.go
deleted file mode 100644
index b64c1de..0000000
--- a/web.go
+++ /dev/null
@@ -1,26 +0,0 @@
1package main
2
3import (
4 "fmt"
5 "net/http"
6 "net/url"
7)
8
9func makeURL(r *http.Request, path string, subs ...interface{}) *url.URL {
10 scheme := "https"
11 if r.TLS == nil {
12 scheme = "http"
13 }
14
15 // Always defer to whatever the proxy told us it was doing because this
16 // could be a mullet-VIP in either direction.
17 if fwProto := r.Header.Get("X-Forwarded-Proto"); fwProto != "" {
18 scheme = fwProto
19 }
20
21 return &url.URL{
22 Scheme: scheme,
23 Host: r.Host,
24 Path: fmt.Sprintf(path, subs...),
25 }
26}
diff --git a/web/config.go b/web/config.go
new file mode 100644
index 0000000..01b6f47
--- /dev/null
+++ b/web/config.go
@@ -0,0 +1,75 @@
1package web
2
3import (
4 "encoding/json"
5 "io/ioutil"
6 "log"
7 "strings"
8
9 "code.crute.me/mcrute/go_ddns_manager/bind"
10 "code.crute.me/mcrute/go_ddns_manager/dns"
11)
12
13type ServerConfig struct {
14 BindConfig *bind.BINDConfig
15 DNSClient *dns.DNSClient
16 AcmeView string
17 DynamicDnsView string
18 DDNSSecrets map[string]string `json:"DDNS"`
19 AcmeSecrets map[string]map[string]int `json:"ACME"`
20}
21
22func LoadServerConfig(zonesFile, secretsFile, server, view string) (*ServerConfig, error) {
23 scfg := &ServerConfig{
24 DNSClient: &dns.DNSClient{Server: server},
25 AcmeView: view,
26 DynamicDnsView: view,
27 }
28
29 cfg, err := bind.ParseBINDConfig(zonesFile)
30 if err != nil {
31 return nil, err
32 }
33 scfg.BindConfig = cfg
34
35 fd, err := ioutil.ReadFile(secretsFile)
36 if err != nil {
37 return nil, err
38 }
39
40 if err = json.Unmarshal(fd, scfg); err != nil {
41 return nil, err
42 }
43
44 return scfg, nil
45}
46
47func (s *ServerConfig) GetDDNSZoneName(k string) string {
48 log.Printf("%#v", s.DDNSSecrets)
49 v, _ := s.DDNSSecrets[k]
50 return v
51}
52
53func (s *ServerConfig) AcmeSecretExists(k string) bool {
54 _, ok := s.AcmeSecrets[k]
55 return ok
56}
57
58func (s *ServerConfig) IsAcmeClientAllowed(key, zone string) bool {
59 u, ok := s.AcmeSecrets[key]
60 if !ok {
61 return false
62 }
63
64 p, ok := u[zone]
65 if ok && p == 1 {
66 return true
67 }
68
69 p, ok = u[strings.TrimRight(zone, ".")]
70 if ok && p == 1 {
71 return true
72 }
73
74 return false
75}
diff --git a/web/controllers/acme.go b/web/controllers/acme.go
new file mode 100644
index 0000000..f40b2ec
--- /dev/null
+++ b/web/controllers/acme.go
@@ -0,0 +1,149 @@
1package controllers
2
3import (
4 "encoding/base64"
5 "encoding/json"
6 "log"
7 "net/http"
8 "strings"
9
10 "github.com/gin-gonic/gin"
11
12 "code.crute.me/mcrute/go_ddns_manager/dns"
13 "code.crute.me/mcrute/go_ddns_manager/web"
14 "code.crute.me/mcrute/go_ddns_manager/web/middleware"
15)
16
17type AcmeChallenge struct {
18 Zone string `json:"zone" binding:"required"`
19 Challenge string `json:"challenge" binding:"required"`
20}
21
22type AcmeChallengeID struct {
23 Zone string
24 Prefix string
25 Challenge string
26}
27
28func DecodeAcmeChallengeID(v string) *AcmeChallengeID {
29 rid, err := base64.URLEncoding.DecodeString(v)
30 if err != nil {
31 return nil
32 }
33
34 var id AcmeChallengeID
35 if err = json.Unmarshal(rid, &id); err != nil {
36 return nil
37 }
38
39 return &id
40}
41
42func (i AcmeChallengeID) Encode() string {
43 id, err := json.Marshal(i)
44 if err != nil {
45 return ""
46 }
47
48 return base64.URLEncoding.EncodeToString(id)
49}
50
51func joinDomainParts(parts ...string) string {
52 p := []string{}
53 for _, i := range parts {
54 if strings.TrimSpace(i) != "" {
55 p = append(p, i)
56 }
57 }
58 return strings.Join(p, ".")
59}
60
61func jsonError(c *gin.Context, status int, msg interface{}) {
62 var o string
63
64 switch msg := msg.(type) {
65 case string:
66 o = msg
67 case error:
68 o = msg.Error()
69 }
70
71 c.JSON(status, gin.H{"error": o})
72}
73
74func CreateAcmeChallenge(c *gin.Context) {
75 cfg := middleware.GetServerConfig(c)
76
77 var ch AcmeChallenge
78 if err := c.ShouldBindJSON(&ch); err != nil {
79 jsonError(c, http.StatusBadRequest, err)
80 return
81 }
82
83 zone, prefix := cfg.BindConfig.FindClosestZone(ch.Zone, cfg.AcmeView)
84 if zone == nil {
85 jsonError(c, http.StatusNotFound, "Zone not found")
86 return
87 }
88
89 if !cfg.IsAcmeClientAllowed(middleware.GetAcmeAuthContext(c), zone.Name) {
90 jsonError(c, http.StatusForbidden, "Zone update not allowed")
91 return
92 }
93
94 txn := cfg.DNSClient.StartUpdate(zone).Upsert(&dns.TXT{
95 Name: joinDomainParts("_acme-challenge", prefix),
96 Ttl: 5,
97 Txt: []string{ch.Challenge},
98 })
99
100 if err := cfg.DNSClient.SendUpdate(txn); err != nil {
101 log.Printf("error Insert: %s", err)
102 jsonError(c, http.StatusInternalServerError, err)
103 return
104 }
105
106 url := web.MakeURL(c.Request, "/acme/%s", AcmeChallengeID{
107 Zone: zone.Name,
108 Prefix: prefix,
109 Challenge: ch.Challenge,
110 }.Encode()).String()
111
112 c.Writer.Header().Set("Location", url)
113 c.JSON(http.StatusCreated, gin.H{"created": url})
114}
115
116func DeleteAcmeChallenge(c *gin.Context) {
117 cfg := middleware.GetServerConfig(c)
118
119 id := DecodeAcmeChallengeID(c.Param("id"))
120 if id == nil {
121 jsonError(c, http.StatusBadRequest, "unable to decode ID")
122 return
123 }
124
125 zone := cfg.BindConfig.Zone(cfg.AcmeView, id.Zone)
126 if zone == nil {
127 jsonError(c, http.StatusNotFound, "Zone not found")
128 return
129 }
130
131 if !cfg.IsAcmeClientAllowed(middleware.GetAcmeAuthContext(c), zone.Name) {
132 jsonError(c, http.StatusForbidden, "Zone update not allowed")
133 return
134 }
135
136 txn := cfg.DNSClient.StartUpdate(zone).Remove(&dns.TXT{
137 Name: joinDomainParts("_acme-challenge", id.Prefix),
138 Ttl: 5,
139 Txt: []string{id.Challenge},
140 })
141
142 if err := cfg.DNSClient.SendUpdate(txn); err != nil {
143 log.Printf("error Remove: %s", err)
144 jsonError(c, http.StatusInternalServerError, err)
145 return
146 }
147
148 c.String(http.StatusNoContent, "")
149}
diff --git a/web/controllers/ddns.go b/web/controllers/ddns.go
new file mode 100644
index 0000000..7300989
--- /dev/null
+++ b/web/controllers/ddns.go
@@ -0,0 +1,51 @@
1package controllers
2
3import (
4 "log"
5 "net/http"
6
7 "github.com/gin-gonic/gin"
8
9 "code.crute.me/mcrute/go_ddns_manager/dns"
10 "code.crute.me/mcrute/go_ddns_manager/web"
11 "code.crute.me/mcrute/go_ddns_manager/web/middleware"
12)
13
14func UpdateDynamicDNS(c *gin.Context) {
15 cfg := middleware.GetServerConfig(c)
16
17 res := middleware.GetDDNSAuthKey(c)
18 if res == "" {
19 log.Println("ddns: Unable to get auth key")
20 c.AbortWithStatus(http.StatusForbidden)
21 return
22 }
23
24 zone, part := cfg.BindConfig.FindClosestZone(res, cfg.DynamicDnsView)
25 if zone == nil {
26 log.Println("ddns: Unable to locate zone")
27 c.AbortWithStatus(http.StatusNotFound)
28 return
29 }
30
31 inip := web.GetRequestIP(c)
32 if inip == nil {
33 log.Println("ddns: Unable to parse IP")
34 c.AbortWithStatus(http.StatusInternalServerError)
35 return
36 }
37
38 txn := cfg.DNSClient.StartUpdate(zone).Upsert(&dns.A{
39 Name: part,
40 Ttl: 60,
41 A: inip,
42 })
43
44 if err := cfg.DNSClient.SendUpdate(txn); err != nil {
45 log.Printf("ddns: %s", err)
46 c.AbortWithStatus(http.StatusInternalServerError)
47 return
48 }
49
50 c.String(http.StatusAccepted, "")
51}
diff --git a/web/controllers/reflect_ip.go b/web/controllers/reflect_ip.go
new file mode 100644
index 0000000..d04b98c
--- /dev/null
+++ b/web/controllers/reflect_ip.go
@@ -0,0 +1,19 @@
1package controllers
2
3import (
4 "net/http"
5
6 "github.com/gin-gonic/gin"
7
8 "code.crute.me/mcrute/go_ddns_manager/web"
9)
10
11func ReflectIP(c *gin.Context) {
12 ip := web.GetRequestIP(c)
13 if ip == nil {
14 c.String(http.StatusInternalServerError, "")
15 return
16 }
17
18 c.String(http.StatusOK, ip.String())
19}
diff --git a/web/middleware/acme.go b/web/middleware/acme.go
new file mode 100644
index 0000000..54a4dfc
--- /dev/null
+++ b/web/middleware/acme.go
@@ -0,0 +1,33 @@
1package middleware
2
3import (
4 "net/http"
5
6 "github.com/gin-gonic/gin"
7)
8
9const acmeUserId = "ACMEAuthUserID"
10
11func AcmeAuthMiddleware(c *gin.Context) {
12 cfg := GetServerConfig(c)
13
14 _, pwd, ok := c.Request.BasicAuth()
15 if !ok {
16 c.Request.Header.Set("WWW-Authenticate", `Basic realm="closed site"`)
17 c.AbortWithStatus(http.StatusUnauthorized)
18 return
19 }
20
21 if !cfg.AcmeSecretExists(pwd) {
22 c.AbortWithStatus(http.StatusForbidden)
23 return
24 } else {
25 c.Set(acmeUserId, pwd)
26 }
27
28 c.Next()
29}
30
31func GetAcmeAuthContext(c *gin.Context) string {
32 return c.GetString(acmeUserId)
33}
diff --git a/web/middleware/config_context.go b/web/middleware/config_context.go
new file mode 100644
index 0000000..d474022
--- /dev/null
+++ b/web/middleware/config_context.go
@@ -0,0 +1,26 @@
1package middleware
2
3import (
4 "github.com/gin-gonic/gin"
5
6 "code.crute.me/mcrute/go_ddns_manager/web"
7)
8
9const serverConfig = "ServerConfig"
10
11func ConfigContextMiddleware(cfg *web.ServerConfig) func(*gin.Context) {
12 return func(c *gin.Context) {
13 c.Set(serverConfig, cfg)
14 c.Next()
15 }
16}
17
18func GetServerConfig(c *gin.Context) *web.ServerConfig {
19 v, ok := c.Get(serverConfig)
20 if !ok {
21 // This should never happen if the config context middlware is in place
22 panic("Unable to get config from request")
23 }
24
25 return v.(*web.ServerConfig)
26}
diff --git a/web/middleware/ddns.go b/web/middleware/ddns.go
new file mode 100644
index 0000000..b213926
--- /dev/null
+++ b/web/middleware/ddns.go
@@ -0,0 +1,40 @@
1package middleware
2
3import (
4 "log"
5 "net/http"
6
7 "github.com/gin-gonic/gin"
8)
9
10const DDNS_AUTH_KEY = "DDNSAuthZone"
11
12type DDNSUpdateRequest struct {
13 Key string `form:"key" binding:"required"`
14}
15
16func DDNSAuthMiddleware(c *gin.Context) {
17 cfg := GetServerConfig(c)
18
19 var req DDNSUpdateRequest
20 if err := c.ShouldBind(&req); err != nil {
21 log.Println("ddnsAuthMiddleware: No key in request")
22 c.AbortWithStatus(http.StatusNotFound)
23 return
24 }
25
26 res := cfg.GetDDNSZoneName(req.Key)
27 if res == "" {
28 log.Println("ddnsAuthMiddleware: Unknown secret")
29 c.AbortWithStatus(http.StatusNotFound)
30 return
31 } else {
32 c.Set(DDNS_AUTH_KEY, res)
33 }
34
35 c.Next()
36}
37
38func GetDDNSAuthKey(c *gin.Context) string {
39 return c.GetString(DDNS_AUTH_KEY)
40}
diff --git a/web/utils.go b/web/utils.go
new file mode 100644
index 0000000..5467132
--- /dev/null
+++ b/web/utils.go
@@ -0,0 +1,117 @@
1package web
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "net"
8 "net/http"
9 "net/url"
10 "os"
11 "os/signal"
12 "regexp"
13
14 "github.com/gin-gonic/gin"
15)
16
17// Parses an IPv4 or IPv6 address with an optional port on the end. Returns
18// match groups for the addresses. The first match is the IPv6 address and the
19// second the IPv4 address.
20var ipRegexp = regexp.MustCompile(`(?:\[([0-9a-f:]+)\]|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(?::\d+)?`)
21
22// TODO: use from a common package
23func ParseIP(s string) net.IP {
24 ips := ipRegexp.FindStringSubmatch(s)
25 if ips == nil {
26 return nil
27 }
28
29 if v6, v4 := ips[1], ips[2]; v6 != "" {
30 return net.ParseIP(v6)
31 } else {
32 return net.ParseIP(v4)
33 }
34}
35
36// TODO: use from a common package
37func GetRequestIP(c *gin.Context) net.IP {
38 if xff := c.Request.Header.Get("X-Forwarded-For"); xff != "" {
39 return ParseIP(xff)
40 }
41 return ParseIP(c.Request.RemoteAddr)
42}
43
44// TODO: use from a common package
45func MakeURL(r *http.Request, path string, subs ...interface{}) *url.URL {
46 scheme := "https"
47 if r.TLS == nil {
48 scheme = "http"
49 }
50
51 // Always defer to whatever the proxy told us it was doing because this
52 // could be a mullet-VIP in either direction.
53 if fwProto := r.Header.Get("X-Forwarded-Proto"); fwProto != "" {
54 scheme = fwProto
55 }
56
57 return &url.URL{
58 Scheme: scheme,
59 Host: r.Host,
60 Path: fmt.Sprintf(path, subs...),
61 }
62}
63
64// TODO: use from a common package
65// Copied from: https://github.com/gin-gonic/gin/blob/59ab588bf597f9f41faee4f217b5659893c2e925/utils.go#L137
66func resolveAddress(addr []string) string {
67 switch len(addr) {
68 case 0:
69 if port := os.Getenv("PORT"); port != "" {
70 log.Printf("Environment variable PORT=\"%s\"", port)
71 return ":" + port
72 }
73 log.Printf("Environment variable PORT is undefined. Using port :8080 by default")
74 return ":8080"
75 case 1:
76 return addr[0]
77 default:
78 panic("too many parameters")
79 }
80}
81
82// TODO: use from a common package
83// Runs a gin.Engine instance in a way that can be canceled by an SIGINT
84func GinRun(e *gin.Engine, debug bool, a ...string) {
85 if debug {
86 gin.SetMode(gin.DebugMode)
87 } else {
88 gin.SetMode(gin.ReleaseMode)
89 }
90
91 srv := &http.Server{
92 Addr: resolveAddress(a),
93 Handler: e,
94 }
95
96 idleConnsClosed := make(chan struct{})
97
98 go func() {
99 sigint := make(chan os.Signal, 1)
100 signal.Notify(sigint, os.Interrupt)
101 <-sigint
102
103 log.Println("Caught SIGINT, shutting down")
104 if err := srv.Shutdown(context.Background()); err != nil {
105 log.Printf("HTTP server Shutdown: %v", err)
106 }
107
108 close(idleConnsClosed)
109 }()
110
111 log.Printf("Listening and serving HTTP on %s\n", srv.Addr)
112 if err := srv.ListenAndServe(); err != http.ErrServerClosed {
113 log.Fatalf("HTTP server ListenAndServe: %v", err)
114 }
115
116 <-idleConnsClosed
117}