commit 138f7ba4c99c85bff94baef6c589fb54c06f14be Author: Kegan Myers Date: Sat Feb 19 14:15:18 2022 -0600 initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf8a71f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Kegan Myers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/db.go b/db.go new file mode 100644 index 0000000..8177f1c --- /dev/null +++ b/db.go @@ -0,0 +1,95 @@ +package main + +import ( + "sync" + "time" +) + +func init() { + db = NewDB() +} + +type DB struct { + data map[string]map[string]time.Time + lock *sync.Mutex + timeout time.Duration +} + +func (db *DB) Lookup(name string) []string { + now := time.Now() + db.lock.Lock() + defer db.lock.Unlock() + + if domain, ok := db.data[name]; ok { + if len(domain) > 0 { + rv := make([]string, 0, len(domain)) + for value, expiresAt := range domain { + if expiresAt.After(now) { + rv = append(rv, value) + } else { + delete(domain, value) + } + } + return rv + } else { + delete(db.data, name) + } + } + return []string{} +} + +func (db *DB) Add(name, value string) { + expiresAt := time.Now().Add(db.timeout) + db.lock.Lock() + defer db.lock.Unlock() + + if _, ok := db.data[name]; ok { + db.data[name][value] = expiresAt + } else { + db.data[name] = map[string]time.Time{value: expiresAt} + } +} + +func (db *DB) Remove(name, value string) { + db.lock.Lock() + defer db.lock.Unlock() + + if domain, ok := db.data[name]; ok { + if _, ok := domain[value]; ok && len(domain) == 1 { + delete(db.data, name) + } else { + delete(domain, value) + } + } +} + +func (db *DB) GC() { + now := time.Now() + db.lock.Lock() + defer db.lock.Unlock() + for name, domain := range db.data { + for value, expiresAt := range domain { + if expiresAt.Before(now) { + delete(domain, value) + } + } + if len(domain) == 0 { + delete(db.data, name) + } + } +} + +func NewDB() *DB { + db := &DB{ + data: map[string]map[string]time.Time{}, + lock: &sync.Mutex{}, + timeout: 2 * time.Minute, + } + go func() { + for { + time.Sleep(5 * db.timeout) + db.GC() + } + }() + return db +} diff --git a/dns.go b/dns.go new file mode 100644 index 0000000..e0d0469 --- /dev/null +++ b/dns.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "strings" + "time" + + "github.com/miekg/dns" +) + +func init() { + dns.HandleFunc(BASE_DOMAIN, serve) +} + +func q_String(q dns.Question) string { + return "" + dns.ClassToString[q.Qclass] + " " + dns.TypeToString[q.Qtype] + " " + q.Name +} + +func serve(w dns.ResponseWriter, r *dns.Msg) { + qStr := "" + for _, q := range r.Question { + qStr += "\n" + q_String(q) + } + fmt.Printf("got question from %s:%s\n", w.RemoteAddr().String(), qStr) + + m := new(dns.Msg) + defer w.WriteMsg(m) + + if r.Opcode != dns.OpcodeQuery && len(r.Question) != 1 { + m.SetRcode(r, dns.RcodeRefused) + return + } + + q := r.Question[0] + qName := strings.ToLower(q.Name) + qKey := qName[0 : len(q.Name)-len(BASE_DOMAIN)] + + m.SetReply(r) + m.Authoritative = true + m.Compress = false + soa := fmt.Sprintf("%s IN SOA %s hostmaster.%s %d 5 1 1 5", qName, DNS_SERVER, DNS_SERVER, (time.Now().Unix() % 4294967295)) + if soaRR, err := dns.NewRR(soa); err == nil { + m.Ns = append(m.Answer, soaRR) + } + + if q.Qtype == dns.TypeTXT { + for _, rData := range db.Lookup(qKey) { + if rr, err := dns.NewRR(fmt.Sprintf("$TTL 5\n%s TXT %s", qName, rData)); err == nil { + m.Answer = append(m.Answer, rr) + } else { + fmt.Printf("error building rr: %s\n", err) + } + } + + rrStr := "" + for _, rr := range m.Answer { + rrStr += "\n" + rr.String() + } + fmt.Printf("sending response for %s:%s\n", qName, rrStr) + return + } else { + fmt.Printf("no records for name %s\n", qName) + } + + m.SetRcode(r, dns.RcodeNameError) + fmt.Printf("%s\n", m.String()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..edfbd1b --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module git.keganmyers.com/terribleplan/acme-dns-httpreq + +go 1.17 + +require ( + github.com/miekg/dns v1.1.46 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7e94ee7 --- /dev/null +++ b/go.sum @@ -0,0 +1,34 @@ +github.com/miekg/dns v1.1.46 h1:uzwpxRtSVxtcIZmz/4Uz6/Rn7G11DvsaslXoy5LxQio= +github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/http.go b/http.go new file mode 100644 index 0000000..4d51fdc --- /dev/null +++ b/http.go @@ -0,0 +1,66 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func init() { + http.HandleFunc("/present", present) + http.HandleFunc("/cleanup", cleanup) +} + +type request struct { + FQDN string `json:"fqdn"` + Value string `json:"value"` +} + +func httpCommon(res http.ResponseWriter, req *http.Request, fn func(request)) { + if req.Method != http.MethodPost { + res.WriteHeader(http.StatusBadRequest) + return + } + + user, _, basicAuthOk := req.BasicAuth() + if !basicAuthOk { + fmt.Printf("basic auth failed\n") + res.WriteHeader(http.StatusForbidden) + return + } + + body := request{} + d := json.NewDecoder(req.Body) + d.DisallowUnknownFields() + if err := d.Decode(&body); err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + + if d.More() { + res.WriteHeader(http.StatusBadRequest) + return + } + + if user != body.FQDN { + fmt.Printf("expected %s == %s\n", user, body.FQDN) + res.WriteHeader(http.StatusForbidden) + return + } + + fn(body) +} + +func present(res http.ResponseWriter, req *http.Request) { + httpCommon(res, req, func(body request) { + fmt.Printf("add %s to %s\n", body.Value, body.FQDN) + db.Add(body.FQDN, body.Value) + }) +} + +func cleanup(res http.ResponseWriter, req *http.Request) { + httpCommon(res, req, func(body request) { + fmt.Printf("remove %s from %s\n", body.Value, body.FQDN) + db.Remove(body.FQDN, body.Value) + }) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..7880bef --- /dev/null +++ b/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/miekg/dns" +) + +var BASE_DOMAIN string +var DNS_SERVER string + +var db *DB + +func init() { + if BASE_DOMAIN == "" { + BASE_DOMAIN = "misconfigured.acme-dns.example.com." + } + if DNS_SERVER == "" { + DNS_SERVER = "misconfigured.acme-dns.example.com." + } +} + +func runServer(server interface { + ListenAndServe() error +}, name string) { + fmt.Printf("starting %s listener\n", name) + if err := server.ListenAndServe(); err != nil { + fmt.Printf("got error in %s listener: %s\n", name, err) + os.Exit(1) + } + fmt.Printf("%s listener stopped\n", name) + os.Exit(1) +} + +func main() { + go runServer(&http.Server{}, "http") + go runServer(&dns.Server{Net: "tcp"}, "dns-tcp") + go runServer(&dns.Server{Net: "udp"}, "dns-udp") + + sig := make(chan os.Signal) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + os.Exit(0) +}