summaryrefslogtreecommitdiff
path: root/main.go
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 /main.go
parentb4214b63d7c73cb0fec55b4e678a98327a159d48 (diff)
downloadgo_ddns_manager-8ed9671c0b4f78711858448cf3b4ee9af0eba51e.tar.bz2
go_ddns_manager-8ed9671c0b4f78711858448cf3b4ee9af0eba51e.tar.xz
go_ddns_manager-8ed9671c0b4f78711858448cf3b4ee9af0eba51e.zip
Refactor into an application
Diffstat (limited to 'main.go')
-rw-r--r--main.go429
1 files changed, 66 insertions, 363 deletions
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}