diff options
-rw-r--r-- | dns/cilent.go | 78 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 33 | ||||
-rw-r--r-- | main.go | 429 | ||||
-rw-r--r-- | web.go | 26 | ||||
-rw-r--r-- | web/config.go | 75 | ||||
-rw-r--r-- | web/controllers/acme.go | 149 | ||||
-rw-r--r-- | web/controllers/ddns.go | 51 | ||||
-rw-r--r-- | web/controllers/reflect_ip.go | 19 | ||||
-rw-r--r-- | web/middleware/acme.go | 33 | ||||
-rw-r--r-- | web/middleware/config_context.go | 26 | ||||
-rw-r--r-- | web/middleware/ddns.go | 40 | ||||
-rw-r--r-- | web/utils.go | 117 |
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 @@ | |||
1 | package dns | 1 | package dns |
2 | 2 | ||
3 | import ( | 3 | import ( |
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 | ||
8 | type DNSClient struct { | 9 | type DNSClient struct { |
9 | Server string | 10 | Server string |
10 | } | 11 | } |
11 | 12 | ||
13 | type DNSTransaction struct { | ||
14 | zone *bind.Zone | ||
15 | key *bind.Key | ||
16 | msg *dns.Msg | ||
17 | } | ||
18 | |||
19 | func (t *DNSTransaction) Upsert(rrs ...RR) *DNSTransaction { | ||
20 | t.RemoveAll(rrs...).Insert(rrs...) | ||
21 | return t | ||
22 | } | ||
23 | |||
24 | func (t *DNSTransaction) Insert(rrs ...RR) *DNSTransaction { | ||
25 | t.msg.Insert(toRRSet(t.zone, rrs...)) | ||
26 | return t | ||
27 | } | ||
28 | |||
29 | func (t *DNSTransaction) Remove(rrs ...RR) *DNSTransaction { | ||
30 | t.msg.Remove(toRRSet(t.zone, rrs...)) | ||
31 | return t | ||
32 | } | ||
33 | |||
34 | func (t *DNSTransaction) RemoveAll(rrs ...RR) *DNSTransaction { | ||
35 | t.msg.RemoveRRset(toRRSet(t.zone, rrs...)) | ||
36 | return t | ||
37 | } | ||
38 | |||
12 | func (c *DNSClient) AXFR(zone *bind.Zone) (chan *dns.Envelope, error) { | 39 | func (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 | ||
23 | func (c *DNSClient) Insert(zone *bind.Zone, rrs ...RR) error { | 50 | func (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 | ||
39 | func (c *DNSClient) Remove(zone *bind.Zone, rrs ...RR) error { | 61 | func (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 | |||
55 | func (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 | ||
@@ -5,5 +5,6 @@ go 1.13 | |||
5 | require ( | 5 | require ( |
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 | ) |
@@ -1,6 +1,13 @@ | |||
1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||
2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= | ||
3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= | ||
4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= | ||
5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= | ||
6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= | ||
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
10 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||
4 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | 11 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= |
5 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | 12 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= |
6 | github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= | 13 | github.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 | |||
12 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= | 19 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= |
13 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | 20 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
14 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | 21 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= |
22 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | ||
23 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= | ||
24 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= | ||
15 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | 25 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= |
16 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= | 26 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= |
17 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= | 27 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= |
28 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= | ||
18 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= | 29 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= |
19 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= | 30 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= |
20 | github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= | 31 | github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= |
21 | github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= | 32 | github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= |
33 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= | ||
34 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= | ||
35 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= | ||
22 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | 36 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= |
37 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= | ||
23 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | 38 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= |
39 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= | ||
24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
42 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= | ||
43 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= | ||
44 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= | ||
45 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= | ||
46 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= | ||
47 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= | ||
48 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= | ||
49 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= | ||
50 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= | ||
26 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | 51 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
52 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||
27 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | 53 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
28 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | 54 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= |
29 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | 55 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
30 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= | 56 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= |
31 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= | 57 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= |
58 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= | ||
32 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= | 59 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= |
33 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= | 60 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= |
61 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= | ||
62 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||
34 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | 63 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
35 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= | 64 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= |
36 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= | 65 | golang.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 | |||
40 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | 69 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
41 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= | 70 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= |
42 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | 71 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
72 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
43 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | 73 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
44 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | 74 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
45 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | 75 | golang.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 | |||
47 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | 77 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
48 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | 78 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
49 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | 79 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= |
50 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= | ||
51 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | 80 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= |
52 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | 81 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
53 | golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | 82 | golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
54 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | 83 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
55 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
86 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= | ||
56 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= | 87 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= |
57 | gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= | 88 | gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= |
58 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= | 89 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= |
@@ -1,395 +1,98 @@ | |||
1 | package main | 1 | package main |
2 | 2 | ||
3 | import ( | 3 | import ( |
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 | |||
18 | const ( | ||
19 | ACME_AUTH_KEY = "ACMEAuthUserID" | ||
20 | DDNS_AUTH_KEY = "DDNSAuthZone" | ||
21 | CTX_SERVER_CONFIG = "ServerConfig" | ||
22 | ) | ||
23 | 11 | ||
24 | var ( | 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 | ||
28 | type ServerConfig struct { | 17 | // TODO: use from a common package |
29 | BindConfig *bind.BINDConfig | 18 | func 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 | |||
35 | func 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 | ||
59 | func (s *ServerConfig) GetDDNZoneName(k string) string { | 24 | return f.Value.String() |
60 | v, _ := s.ddnsSecrets[k] | ||
61 | return v | ||
62 | } | 25 | } |
63 | 26 | ||
64 | func (s *ServerConfig) AcmeSecretExists(k string) bool { | 27 | // TODO: use from a common package |
65 | _, ok := s.acmeSecrets[k] | 28 | func 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)) | |
69 | func (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 | |||
88 | type DDNSUpdateRequest struct { | ||
89 | Key string `form:"key" binding:"required"` | ||
90 | } | ||
91 | |||
92 | type AcmeChallenge struct { | ||
93 | Zone string `json:"zone" binding:"required"` | ||
94 | Challenge string `json:"challenge" binding:"required"` | ||
95 | } | ||
96 | |||
97 | type AcmeChallengeID struct { | ||
98 | Zone string | ||
99 | Prefix string | ||
100 | Challenge string | ||
101 | } | ||
102 | |||
103 | func 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 | |||
113 | func 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 | |||
184 | func 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 | |||
235 | func 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 | |||
286 | func 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 | |||
309 | func 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 | |||
329 | func 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 | |||
351 | func 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 | |||
358 | func 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 | ||
368 | func main() { | 42 | func 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 | |||
64 | func 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 | } |
@@ -1,26 +0,0 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "net/http" | ||
6 | "net/url" | ||
7 | ) | ||
8 | |||
9 | func 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 @@ | |||
1 | package web | ||
2 | |||
3 | import ( | ||
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 | |||
13 | type 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 | |||
22 | func 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 | |||
47 | func (s *ServerConfig) GetDDNSZoneName(k string) string { | ||
48 | log.Printf("%#v", s.DDNSSecrets) | ||
49 | v, _ := s.DDNSSecrets[k] | ||
50 | return v | ||
51 | } | ||
52 | |||
53 | func (s *ServerConfig) AcmeSecretExists(k string) bool { | ||
54 | _, ok := s.AcmeSecrets[k] | ||
55 | return ok | ||
56 | } | ||
57 | |||
58 | func (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 @@ | |||
1 | package controllers | ||
2 | |||
3 | import ( | ||
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 | |||
17 | type AcmeChallenge struct { | ||
18 | Zone string `json:"zone" binding:"required"` | ||
19 | Challenge string `json:"challenge" binding:"required"` | ||
20 | } | ||
21 | |||
22 | type AcmeChallengeID struct { | ||
23 | Zone string | ||
24 | Prefix string | ||
25 | Challenge string | ||
26 | } | ||
27 | |||
28 | func 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 | |||
42 | func (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 | |||
51 | func 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 | |||
61 | func 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 | |||
74 | func 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 | |||
116 | func 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 @@ | |||
1 | package controllers | ||
2 | |||
3 | import ( | ||
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 | |||
14 | func 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 @@ | |||
1 | package controllers | ||
2 | |||
3 | import ( | ||
4 | "net/http" | ||
5 | |||
6 | "github.com/gin-gonic/gin" | ||
7 | |||
8 | "code.crute.me/mcrute/go_ddns_manager/web" | ||
9 | ) | ||
10 | |||
11 | func 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 @@ | |||
1 | package middleware | ||
2 | |||
3 | import ( | ||
4 | "net/http" | ||
5 | |||
6 | "github.com/gin-gonic/gin" | ||
7 | ) | ||
8 | |||
9 | const acmeUserId = "ACMEAuthUserID" | ||
10 | |||
11 | func 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 | |||
31 | func 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 @@ | |||
1 | package middleware | ||
2 | |||
3 | import ( | ||
4 | "github.com/gin-gonic/gin" | ||
5 | |||
6 | "code.crute.me/mcrute/go_ddns_manager/web" | ||
7 | ) | ||
8 | |||
9 | const serverConfig = "ServerConfig" | ||
10 | |||
11 | func ConfigContextMiddleware(cfg *web.ServerConfig) func(*gin.Context) { | ||
12 | return func(c *gin.Context) { | ||
13 | c.Set(serverConfig, cfg) | ||
14 | c.Next() | ||
15 | } | ||
16 | } | ||
17 | |||
18 | func 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 @@ | |||
1 | package middleware | ||
2 | |||
3 | import ( | ||
4 | "log" | ||
5 | "net/http" | ||
6 | |||
7 | "github.com/gin-gonic/gin" | ||
8 | ) | ||
9 | |||
10 | const DDNS_AUTH_KEY = "DDNSAuthZone" | ||
11 | |||
12 | type DDNSUpdateRequest struct { | ||
13 | Key string `form:"key" binding:"required"` | ||
14 | } | ||
15 | |||
16 | func 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 | |||
38 | func 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 @@ | |||
1 | package web | ||
2 | |||
3 | import ( | ||
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. | ||
20 | var 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 | ||
23 | func 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 | ||
37 | func 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 | ||
45 | func 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 | ||
66 | func 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 | ||
84 | func 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 | } | ||