initial commit
This commit is contained in:
commit
138f7ba4c9
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -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.
|
95
db.go
Normal file
95
db.go
Normal file
|
@ -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
|
||||||
|
}
|
67
dns.go
Normal file
67
dns.go
Normal file
|
@ -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())
|
||||||
|
}
|
12
go.mod
Normal file
12
go.mod
Normal file
|
@ -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
|
||||||
|
)
|
34
go.sum
Normal file
34
go.sum
Normal file
|
@ -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=
|
66
http.go
Normal file
66
http.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
48
main.go
Normal file
48
main.go
Normal file
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in a new issue