Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Ben
adf836979a - native go sqlite
- native dns server instead of bind
2022-06-10 13:44:52 +02:00
24 changed files with 829 additions and 531 deletions

View file

@ -1,7 +1,6 @@
MIT License
Copyright (c) 2020 Benjamin Bärthlein
Copyright (c) 2016 David Prandzioch
Copyright (c) 2022 Benjamin Bärthlein
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -8,7 +8,9 @@
![Go version](https://img.shields.io/github/go-mod/go-version/benjaminbear/docker-ddns-server?filename=dyndns%2Fgo.mod)
![License](https://img.shields.io/github/license/benjaminbear/docker-ddns-server)
With docker-ddns-server you can setup your own dynamic DNS server. This project is inspired by https://github.com/dprandzioch/docker-ddns . In addition to the original version, you can setup and maintain your dyndns entries via simple web ui.
With docker-ddns-server you can setup your own dynamic DNS server. You can setup and maintain your dyndns entries via simple web ui.
The application uses its own dns server and has no dependencies to C or the os. In general you can compile
and run it on every os and architecture supported by the golang compiler.
<p float="left">
<img src="https://raw.githubusercontent.com/benjaminbear/docker-ddns-server/master/img/addhost.png" width="285">
@ -20,6 +22,15 @@ With docker-ddns-server you can setup your own dynamic DNS server. This project
You can either take the docker image or build it on your own.
### Using as binary
You can build and install it on your own
```
go install github.com/benjaminbear/docker-ddns-server/v2@latest
```
or download the package on the release page.
Don't forget to add the static and views folder from the repository.
### Using the docker image
Just customize this to your needs and run:

View file

@ -1,30 +1,17 @@
FROM golang:1.18 as builder
ENV GO111MODULE=on
ENV GOPATH=/root/go
RUN mkdir -p /root/go/src
COPY dyndns /root/go/src/dyndns
WORKDIR /root/go/src/dyndns
# temp sqlite3 error fix
ENV CGO_CFLAGS "-g -O2 -Wno-return-local-addr"
RUN go mod tidy
RUN GOOS=linux go build -o /root/go/bin/dyndns && go test -v
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /root/go/bin/dyndns && go test -v
FROM debian:11-slim
FROM scratch
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -q -y bind9 dnsutils curl && \
apt-get clean
RUN chmod 770 /var/cache/bind
COPY deployment/setup.sh /root/setup.sh
RUN chmod +x /root/setup.sh
COPY deployment/named.conf.options /etc/bind/named.conf.options
WORKDIR /root
COPY --from=builder /root/go/bin/dyndns /root/dyndns
COPY dyndns/views /root/views
COPY dyndns/static /root/static
COPY --from=builder /root/go/bin/dyndns /dyndns
COPY dyndns/views /views
COPY dyndns/static /static
EXPOSE 53 8080
CMD ["sh", "-c", "/root/setup.sh ; service named start ; /root/dyndns"]
CMD ["/dyndns"]

View file

@ -1,8 +0,0 @@
options {
directory "/var/cache/bind";
dnssec-validation auto;
recursion no;
allow-transfer { none; };
auth-nxdomain no;
listen-on-v6 { any; };
};

View file

@ -1,51 +0,0 @@
#!/bin/bash
#[ -z "$DDNS_ADMIN_LOGIN" ] && echo "DDNS_ADMIN_LOGIN not set" && exit 1;
[ -z "$DDNS_DOMAINS" ] && echo "DDNS_DOMAINS not set" && exit 1;
[ -z "$DDNS_PARENT_NS" ] && echo "DDNS_PARENT_NS not set" && exit 1;
[ -z "$DDNS_DEFAULT_TTL" ] && echo "DDNS_DEFAULT_TTL not set" && exit 1;
DDNS_IP=$(curl icanhazip.com)
for d in ${DDNS_DOMAINS//,/ }
do
if ! grep 'zone "'$d'"' /etc/bind/named.conf > /dev/null
then
echo "creating zone...";
cat >> /etc/bind/named.conf <<EOF
zone "$d" {
type master;
file "$d.zone";
allow-query { any; };
allow-transfer { none; };
allow-update { localhost; };
};
EOF
fi
if [ ! -f /var/cache/bind/$d.zone ]
then
echo "creating zone file..."
cat > /var/cache/bind/$d.zone <<EOF
\$ORIGIN .
\$TTL 86400 ; 1 day
$d IN SOA ${DDNS_PARENT_NS}. root.${d}. (
74 ; serial
3600 ; refresh (1 hour)
900 ; retry (15 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
NS ${DDNS_PARENT_NS}.
A ${DDNS_IP}
\$ORIGIN ${d}.
\$TTL ${DDNS_DEFAULT_TTL}
EOF
fi
done
# If /var/cache/bind is a volume, permissions are probably not ok
chown root:bind /var/cache/bind
chown bind:bind /var/cache/bind/*
chmod 770 /var/cache/bind
chmod 644 /var/cache/bind/*

99
dyndns/config/config.go Normal file
View file

@ -0,0 +1,99 @@
package config
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"github.com/caarlos0/env/v6"
)
type Config struct {
AdminLogin string `env:"ADMIN_LOGIN"`
Title string `env:"TITLE" envDefault:"TheBBCloud DynDNS"`
LogoutUrl string `env:"LOGOUT_URL"`
ClearLogInterval int `env:"CLEAR_LOG_INTERVAL"`
Domains []string `env:"DOMAINS,notEmpty" envSeparator:";"`
ParentNS string `env:"PARENT_NS,notEmpty"`
DefaultTTL int `env:"DEFAULT_TTL,notEmpty"`
AllowWildcard bool `env:"ALLOW_WILDCARD"`
ExternalIP net.IP `env:"EXTERNAL_IP"`
ExternalIPResolver url.URL `env:"EXTERNAL_IP_RESOLVER" envDefault:"http://icanhazip.com"`
}
// ParseEnvs parses all needed environment variables:
// DDNS_ADMIN_LOGIN: The basic auth login string in htpasswd style.
// DDNS_DOMAINS: All domains that will be handled by the dyndns server.
func ParseEnvs() (*Config, error) {
fmt.Println("Reading environment variables")
c := &Config{}
err := env.Parse(c, env.Options{Prefix: "DDNS_"})
if err != nil {
return c, err
}
err = c.validateExternalIP()
if err != nil {
return c, err
}
c.printConfig()
return c, err
}
func (c *Config) printConfig() {
if c.AdminLogin == "" {
fmt.Println("No Auth! DDNS_ADMIN_LOGIN should be set")
}
if c.AllowWildcard {
fmt.Println("Wildcard allowed")
}
fmt.Println("External IP set:", c.ExternalIP)
fmt.Println("External IP Resolver:", c.ExternalIPResolver.String())
fmt.Println("Domains:", c.Domains)
fmt.Println("Parent Namespace:", c.ParentNS)
fmt.Println("Default TTL:", c.DefaultTTL)
fmt.Println("Web UI Title:", c.Title)
if c.LogoutUrl != "" {
fmt.Println("Logout URL set:", c.LogoutUrl)
}
if c.ClearLogInterval > 0 {
fmt.Println("Log clear interval found:", c.ClearLogInterval, "days")
}
}
// Parse external IP or get it yourself.
func (c *Config) validateExternalIP() error {
if c.ExternalIP != nil {
return nil
}
resp, err := http.Get(c.ExternalIPResolver.String())
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
ip := strings.TrimSpace(string(body))
c.ExternalIP = net.ParseIP(ip)
if c.ExternalIP == nil {
return fmt.Errorf("%s is not a valid ip", ip)
}
return nil
}

View file

@ -1,7 +1,7 @@
package model
package db
import (
"github.com/jinzhu/gorm"
"gorm.io/gorm"
)
// CName is a dns cname entry.

37
dyndns/db/db.go Normal file
View file

@ -0,0 +1,37 @@
package db
import (
"fmt"
"os"
"path/filepath"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
const (
dbDir = "database"
dbFile = "ddns.db"
)
// InitDB creates an empty database and creates all tables if there isn't already one, or opens the existing one.
func InitDB() (*gorm.DB, error) {
if _, err := os.Stat(dbDir); os.IsNotExist(err) {
err = os.MkdirAll(dbDir, os.ModePerm)
if err != nil {
return nil, err
}
}
db, err := gorm.Open(sqlite.Open(filepath.Join(dbDir, dbFile)))
if err != nil {
return db, err
}
path, _ := filepath.Abs(filepath.Join(dbDir, dbFile))
fmt.Println("Database path:", path)
err = db.AutoMigrate(&Host{}, &CName{}, &Log{})
return db, err
}

View file

@ -1,9 +1,10 @@
package model
package db
import (
"fmt"
"time"
"github.com/jinzhu/gorm"
"gorm.io/gorm"
)
// Host is a dns host entry.
@ -34,3 +35,7 @@ func (h *Host) UpdateHost(updateHost *Host) (updateRecord bool) {
return
}
func (h *Host) FullDomain() string {
return fmt.Sprintf("%s.%s", h.Hostname, h.Domain)
}

View file

@ -1,9 +1,9 @@
package model
package db
import (
"time"
"github.com/jinzhu/gorm"
"gorm.io/gorm"
)
// Log defines a log entry.

View file

@ -0,0 +1,64 @@
package dnsserver
import (
"fmt"
"net"
"time"
"github.com/miekg/dns"
)
// IPAnswer creates an A or AAAA entry dns answer.
// Returns error if ip is not a valid ipv4 or ipv6.
func IPAnswer(domainName string, ip net.IP, ttl int) ([]dns.RR, error) {
rrType := ipType(ip.String())
if rrType == dns.TypeNone {
return nil, fmt.Errorf("ip is not a valid ipv4 or ipv6")
}
rrHeader := dns.RR_Header{
Name: domainName,
Rrtype: rrType,
Class: dns.ClassINET,
Ttl: uint32(ttl),
}
if rrType == dns.TypeAAAA {
return []dns.RR{&dns.AAAA{Hdr: rrHeader, AAAA: ip}}, nil
}
return []dns.RR{&dns.A{Hdr: rrHeader, A: ip}}, nil
}
// CNameAnswer creates a CNAME entry dns answer.
func CNameAnswer(domainName string, target string, ttl int) []dns.RR {
rrHeader := dns.RR_Header{
Name: domainName,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: uint32(ttl),
}
return []dns.RR{&dns.CNAME{Hdr: rrHeader, Target: target}}
}
// SOAAnswer creates a SOA entry dns answer.
func SOAAnswer(domainName string, nameServer string, ttl int) []dns.RR {
rrHeader := dns.RR_Header{
Name: domainName,
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
Ttl: uint32(ttl),
}
return []dns.RR{&dns.SOA{
Hdr: rrHeader,
Serial: uint32(time.Now().Unix()),
Refresh: uint32(3600),
Retry: uint32(900),
Expire: uint32(604800),
Minttl: uint32(86400),
Ns: nameServer,
Mbox: "root." + domainName,
}}
}

142
dyndns/dnsserver/handler.go Normal file
View file

@ -0,0 +1,142 @@
package dnsserver
import (
"errors"
"fmt"
"strings"
"github.com/benjaminbear/docker-ddns-server/dyndns/ipparser"
"gorm.io/gorm"
"github.com/benjaminbear/docker-ddns-server/dyndns/config"
"github.com/labstack/gommon/log"
"github.com/miekg/dns"
)
type Handler struct {
Config *config.Config
DB *gorm.DB
}
func (h *Handler) Do(w dns.ResponseWriter, req *dns.Msg) {
for _, q := range req.Question {
log.Infof(prettyQuestion(q))
m := new(dns.Msg)
m.Authoritative = true
m.SetReply(req)
// Refuse if requested domain is not supported
_, _, err := h.checkDomain(UnFqdn(q.Name))
if errors.Is(err, ErrUnsupportedDomain) {
m.Rcode = dns.RcodeRefused
w.WriteMsg(m)
}
// Resolve
answers, err := h.Resolve(q)
if err != nil || len(answers) == 0 {
answers, _ = h.ResolveSOA(q.Name)
m.Ns = append(m.Ns, answers...)
m.Rcode = dns.RcodeNameError
w.WriteMsg(m)
fmt.Println("return SOA as excuse with", answers, err)
return
}
fmt.Println("answer to be added is", answers)
m.Answer = append(m.Answer, answers...)
w.WriteMsg(m)
}
}
func (h *Handler) Resolve(question dns.Question) ([]dns.RR, error) {
emptyAnswers := make([]dns.RR, 0)
if question.Qclass != dns.ClassINET {
return emptyAnswers, nil
}
switch question.Qtype {
case dns.TypeA:
fallthrough
case dns.TypeAAAA:
// ResolveIP
fmt.Println("IPResolve")
ipResolveChain := []func(fqdn string) ([]dns.RR, error){h.ResolveDNSA, h.ResolveA, h.ResolveCName}
for i, resolveFunc := range ipResolveChain {
answers, err := resolveFunc(question.Name)
fmt.Println("finished chain run", i, "with", answers, err)
if err == nil && len(answers) > 0 {
fmt.Println("returning", answers, err)
return answers, err
}
}
fmt.Println("finished ip w/o success")
case dns.TypeCNAME:
fmt.Println("CNameResolve")
answers, err := h.ResolveCName(question.Name)
return answers, err
case dns.TypeTXT:
// return TXT
fmt.Println("TXTResolve")
case dns.TypeSOA:
fmt.Println("SOAResolve")
// ignore errors, it's just empty slices
soaAnswer, _ := h.ResolveSOA(question.Name)
return soaAnswer, nil
}
fmt.Println("returning empty")
return emptyAnswers, nil
}
var ErrUnsupportedDomain = errors.New("domain not supported")
var ErrIsDomain = errors.New("fqdn is domain")
func (h *Handler) checkDomain(fullDomainName string) (hostname string, domain string, err error) {
for _, d := range h.Config.Domains {
if fullDomainName == d {
return hostname, d, ErrIsDomain
}
if strings.HasSuffix(fullDomainName, "."+d) && fullDomainName != "."+d {
hostname = strings.TrimSuffix(fullDomainName, "."+d)
return hostname, d, nil
}
}
return "", "", ErrUnsupportedDomain
}
func UnFqdn(s string) string {
if dns.IsFqdn(s) {
return s[:len(s)-1]
}
return s
}
func ipType(ip string) uint16 {
if ipparser.ValidIP4(ip) {
return dns.TypeA
} else if ipparser.ValidIP6(ip) {
return dns.TypeAAAA
}
return dns.TypeNone
}
func prettyQuestion(question dns.Question) string {
return fmt.Sprintf("request for %s %s %s", question.Name, dns.ClassToString[question.Qclass], dns.TypeToString[question.Qtype])
}

View file

@ -0,0 +1,94 @@
package dnsserver
import (
"errors"
"fmt"
"net"
"github.com/benjaminbear/docker-ddns-server/dyndns/db"
"github.com/miekg/dns"
)
// ResolveDNSA checks if the requested domain is the domain of the ddns webserver
// and returns the corresponding A dns entry.
func (h *Handler) ResolveDNSA(fqdn string) ([]dns.RR, error) {
for _, d := range h.Config.Domains {
if d == UnFqdn(fqdn) {
answers, err := IPAnswer(fqdn, h.Config.ExternalIP, h.Config.DefaultTTL)
return answers, err
}
}
return []dns.RR{}, nil
}
// ResolveA checks if the requested domain is a valid A entry from the database
// and returns the corresponding A dns entry.
func (h *Handler) ResolveA(fqdn string) ([]dns.RR, error) {
qHostname, qDomain, err := h.checkDomain(UnFqdn(fqdn))
if err != nil {
// return SOA
return []dns.RR{}, err
}
hosts := new([]db.Host)
if num := h.DB.Where(&db.Host{Hostname: qHostname, Domain: qDomain}).Find(hosts).RowsAffected; num < 1 {
return []dns.RR{}, nil
}
host := (*hosts)[0]
answers, err := IPAnswer(fqdn, net.ParseIP(host.Ip), host.Ttl)
if err != nil {
// ip not supported
return []dns.RR{}, err
}
return answers, nil
}
// ResolveCName checks if the requested domain is a valid CNAME entry from the database
// and returns the corresponding CNAME and A dns entry.
func (h *Handler) ResolveCName(fqdn string) ([]dns.RR, error) {
qHostname, qDomain, err := h.checkDomain(UnFqdn(fqdn))
if err != nil {
// return SOA
return []dns.RR{}, err
}
cnames := new([]db.CName)
if num := h.DB.Joins("Target").Where(&db.CName{Hostname: qHostname}).Find(cnames, "Target.domain = ?", qDomain).RowsAffected; num < 1 {
return []dns.RR{}, nil
}
cname := (*cnames)[0]
cnameAnswers := CNameAnswer(fqdn, cname.Target.FullDomain()+".", cname.Ttl)
aAnswers, err := IPAnswer(cname.Target.FullDomain()+".", net.ParseIP(cname.Target.Ip), cname.Target.Ttl)
if err != nil {
// return SOA
return []dns.RR{}, err
}
return append(cnameAnswers, aAnswers...), nil
}
func (h *Handler) ResolveSOA(fqdn string) ([]dns.RR, error) {
_, qDomain, err := h.checkDomain(UnFqdn(fqdn))
fmt.Println(qDomain, err)
if errors.Is(err, ErrIsDomain) {
qDomain = UnFqdn(fqdn)
} else if err != nil {
return []dns.RR{}, err
}
for _, d := range h.Config.Domains {
if d == qDomain {
return SOAAnswer(d+".", h.Config.ParentNS+".", h.Config.DefaultTTL), nil
}
}
return []dns.RR{}, fmt.Errorf("requesting for unsupported domain")
}

View file

@ -0,0 +1,56 @@
package dnsserver
import (
"log"
"net"
"strconv"
"time"
"github.com/miekg/dns"
)
type Server struct {
Host string
Port int
RTimeout time.Duration
WTimeout time.Duration
}
func (s *Server) Addr() string {
return net.JoinHostPort(s.Host, strconv.Itoa(s.Port))
}
func (s *Server) Run(handler *Handler) {
tcpHandler := dns.NewServeMux()
tcpHandler.HandleFunc(".", handler.Do)
udpHandler := dns.NewServeMux()
udpHandler.HandleFunc(".", handler.Do)
tcpServer := &dns.Server{Addr: s.Addr(),
Net: "tcp",
Handler: tcpHandler,
ReadTimeout: s.RTimeout,
WriteTimeout: s.WTimeout}
udpServer := &dns.Server{Addr: s.Addr(),
Net: "udp",
Handler: udpHandler,
UDPSize: 65535,
ReadTimeout: s.RTimeout,
WriteTimeout: s.WTimeout}
go s.start(udpServer)
go s.start(tcpServer)
}
func (s *Server) start(ds *dns.Server) {
log.Printf("Start %s listener on %s\n", ds.Net, s.Addr())
err := ds.ListenAndServe()
if err != nil {
log.Fatalf("Start %s listener on %s failed:%s\n", ds.Net, s.Addr(), err.Error())
}
}

View file

@ -3,29 +3,42 @@ module github.com/benjaminbear/docker-ddns-server/dyndns
go 1.18
require (
github.com/caarlos0/env/v6 v6.9.3
github.com/foolin/goview v0.3.0
github.com/glebarez/sqlite v1.4.5
github.com/go-playground/validator/v10 v10.11.0
github.com/jinzhu/gorm v1.9.16
github.com/labstack/echo/v4 v4.7.2
github.com/labstack/gommon v0.3.1
github.com/miekg/dns v1.1.49
github.com/tg123/go-htpasswd v1.2.0
gorm.io/gorm v1.23.5
)
require (
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
github.com/glebarez/go-sqlite v1.17.2 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/net v0.0.0-20220524220425-1d687d428aca // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
modernc.org/libc v1.16.8 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/sqlite v1.17.2 // indirect
)

View file

@ -2,23 +2,24 @@ github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/caarlos0/env/v6 v6.9.3 h1:Tyg69hoVXDnpO5Qvpsu8EoquarbPyQb+YwExWHP8wWU=
github.com/caarlos0/env/v6 v6.9.3/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/foolin/goview v0.3.0 h1:q5wKwXKEFb20dMRfYd59uj5qGCo7q4L9eVHHUjmMWrg=
github.com/foolin/goview v0.3.0/go.mod h1:OC1VHC4FfpWymhShj8L1Tc3qipFmrmm+luAEdTvkos4=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/glebarez/go-sqlite v1.17.2 h1:gyTyFr2RFFQd2gp6fOOdfnTvUn99zwvVOrQFHA4S+DY=
github.com/glebarez/go-sqlite v1.17.2/go.mod h1:lakPjzvnJ6uSIARV+5dPALDuSLL3879PlzHFMEpbceM=
github.com/glebarez/sqlite v1.4.5 h1:oaJupO4X9iTn4sXRvP5Vs15BNvKh9dx5AQfciKlDvV4=
github.com/glebarez/sqlite v1.4.5/go.mod h1:6D+bB+DdXlEC4mO+pUFJWixVcnrHTIAJ9U6Ynnn4Lxk=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
@ -27,21 +28,20 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@ -57,32 +57,35 @@ github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8=
github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0=
github.com/tg123/go-htpasswd v1.2.0/go.mod h1:h7IzlfpvIWnVJhNZ0nQ9HaFxHb7pn5uFJYLlEUJa2sM=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
@ -91,43 +94,58 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8=
golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
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-20190222072716-a9d3bda3a223/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-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190609082536-301114b31cce/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
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/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -135,6 +153,15 @@ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2Y
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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-20191204190536-9bdfabe68543/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -145,3 +172,31 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
modernc.org/libc v1.16.8 h1:Ux98PaOMvolgoFX/YwusFOHBnanXdGRmWgI8ciI2z4o=
modernc.org/libc v1.16.8/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.17.2 h1:TjmF36Wi5QcPYqRoAacV1cAyJ7xB/CD0ExpVUEMebnw=
modernc.org/sqlite v1.17.2/go.mod h1:GOQmuiXd6pTTes1Fi2s9apiCcD/wbKQtBZ0Nw6/etjM=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=

View file

@ -1,200 +0,0 @@
package handler
import (
"fmt"
"github.com/labstack/gommon/log"
"os"
"strconv"
"strings"
"time"
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
"github.com/go-playground/validator/v10"
"github.com/jinzhu/gorm"
"github.com/labstack/echo/v4"
"github.com/tg123/go-htpasswd"
)
type Handler struct {
DB *gorm.DB
AuthAdmin bool
Config Envs
Title string
DisableAdminAuth bool
LastClearedLogs time.Time
ClearInterval uint64
AllowWildcard bool
LogoutUrl string
}
type Envs struct {
AdminLogin string
Domains []string
}
type CustomValidator struct {
Validator *validator.Validate
}
// Validate implements the Validator.
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.Validator.Struct(i)
}
type Error struct {
Message string `json:"message"`
}
// Authenticate is the method the website admin user and the host update user have to authenticate against.
// To gather admin rights the username password combination must match with the credentials given by the env var.
func (h *Handler) AuthenticateUpdate(username, password string, c echo.Context) (bool, error) {
h.CheckClearInterval()
reqParameter := c.QueryParam("hostname")
reqArr := strings.SplitN(reqParameter, ".", 2)
if len(reqArr) != 2 {
log.Error("Error: Something wrong with the hostname parameter")
return false, nil
}
host := &model.Host{}
if err := h.DB.Where(&model.Host{UserName: username, Password: password, Hostname: reqArr[0], Domain: reqArr[1]}).First(host).Error; err != nil {
log.Error("Error: ", err)
return false, nil
}
if host.ID == 0 {
log.Error("hostname or user user credentials unknown")
return false, nil
}
c.Set("updateHost", host)
return true, nil
}
func (h *Handler) AuthenticateAdmin(username, password string, c echo.Context) (bool, error) {
h.AuthAdmin = false
ok, err := h.authByEnv(username, password)
if err != nil {
log.Error("Error:", err)
return false, nil
}
if ok {
h.AuthAdmin = true
return true, nil
}
return false, nil
}
func (h *Handler) authByEnv(username, password string) (bool, error) {
hashReader := strings.NewReader(h.Config.AdminLogin)
pw, err := htpasswd.NewFromReader(hashReader, htpasswd.DefaultSystems, nil)
if err != nil {
return false, err
}
if ok := pw.Match(username, password); ok {
return true, nil
}
return false, nil
}
// ParseEnvs parses all needed environment variables:
// DDNS_ADMIN_LOGIN: The basic auth login string in htpasswd style.
// DDNS_DOMAINS: All domains that will be handled by the dyndns server.
func (h *Handler) ParseEnvs() (adminAuth bool, err error) {
log.Info("Read environment variables")
h.Config = Envs{}
adminAuth = true
h.Config.AdminLogin = os.Getenv("DDNS_ADMIN_LOGIN")
if h.Config.AdminLogin == "" {
log.Info("No Auth! DDNS_ADMIN_LOGIN should be set")
adminAuth = false
h.AuthAdmin = true
h.DisableAdminAuth = true
}
var ok bool
h.Title, ok = os.LookupEnv("DDNS_TITLE")
if !ok {
h.Title = "TheBBCloud DynDNS"
}
allowWildcard, ok := os.LookupEnv("DDNS_ALLOW_WILDCARD")
if ok {
h.AllowWildcard, err = strconv.ParseBool(allowWildcard)
if err == nil {
log.Info("Wildcard allowed")
}
}
logoutUrl, ok := os.LookupEnv("DDNS_LOGOUT_URL")
if ok {
if len(logoutUrl) > 0 {
log.Info("Logout url set: ", logoutUrl)
h.LogoutUrl = logoutUrl
}
}
clearEnv := os.Getenv("DDNS_CLEAR_LOG_INTERVAL")
clearInterval, err := strconv.ParseUint(clearEnv, 10, 32)
if err != nil {
log.Info("No log clear interval found")
} else {
log.Info("log clear interval found: ", clearInterval, "days")
h.ClearInterval = clearInterval
if clearInterval > 0 {
h.LastClearedLogs = time.Now()
}
}
h.Config.Domains = strings.Split(os.Getenv("DDNS_DOMAINS"), ",")
if len(h.Config.Domains) < 1 {
return adminAuth, fmt.Errorf("environment variable DDNS_DOMAINS has to be set")
}
return adminAuth, nil
}
// InitDB creates an empty database and creates all tables if there isn't already one, or opens the existing one.
func (h *Handler) InitDB() (err error) {
if _, err := os.Stat("database"); os.IsNotExist(err) {
err = os.MkdirAll("database", os.ModePerm)
if err != nil {
return err
}
}
h.DB, err = gorm.Open("sqlite3", "database/ddns.db")
if err != nil {
return err
}
if !h.DB.HasTable(&model.Host{}) {
h.DB.CreateTable(&model.Host{})
}
if !h.DB.HasTable(&model.CName{}) {
h.DB.CreateTable(&model.CName{})
}
if !h.DB.HasTable(&model.Log{}) {
h.DB.CreateTable(&model.Log{})
}
return nil
}
// Check if a log cleaning is needed
func (h *Handler) CheckClearInterval() {
if !h.LastClearedLogs.IsZero() {
if !DateEqual(time.Now(), h.LastClearedLogs) {
go h.ClearLogs()
}
}
}
// compare two dates
func DateEqual(date1, date2 time.Time) bool {
y1, m1, d1 := date1.Date()
y2, m2, d2 := date2.Date()
return y1 == y2 && m1 == m2 && d1 == d2
}

View file

@ -1,20 +1,33 @@
package main
import (
"github.com/benjaminbear/docker-ddns-server/dyndns/handler"
"github.com/foolin/goview"
"github.com/foolin/goview/supports/echoview-v4"
"github.com/go-playground/validator/v10"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
"html/template"
"net/http"
"time"
"github.com/benjaminbear/docker-ddns-server/dyndns/db"
"github.com/benjaminbear/docker-ddns-server/dyndns/dnsserver"
"github.com/benjaminbear/docker-ddns-server/dyndns/config"
"github.com/benjaminbear/docker-ddns-server/dyndns/webserver"
"github.com/foolin/goview"
"github.com/foolin/goview/supports/echoview-v4"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
)
func main() {
// Parse Config
conf, err := config.ParseEnvs()
if err != nil {
log.Fatal(err)
}
// Set new instance
e := echo.New()
@ -36,25 +49,20 @@ func main() {
})
// Set Validator
e.Validator = &handler.CustomValidator{Validator: validator.New()}
e.Validator = &webserver.CustomValidator{Validator: validator.New()}
// Set Statics
e.Static("/static", "static")
// Initialize handler
h := &handler.Handler{}
// Database connection
if err := h.InitDB(); err != nil {
e.Logger.Fatal(err)
}
defer h.DB.Close()
authAdmin, err := h.ParseEnvs()
dbConn, err := db.InitDB()
if err != nil {
e.Logger.Fatal(err)
}
// Initialize webserver
h := webserver.New(conf, dbConn)
// UI Routes
groupPublic := e.Group("/")
groupPublic.GET("*", func(c echo.Context) error {
@ -62,7 +70,7 @@ func main() {
return c.Redirect(301, "./admin/")
})
groupAdmin := e.Group("/admin")
if authAdmin {
if conf.AdminLogin != "" {
groupAdmin.Use(middleware.BasicAuth(h.AuthenticateAdmin))
}
@ -82,8 +90,8 @@ func main() {
//redirect to logout
groupAdmin.GET("/logout", func(c echo.Context) error {
// either custom url
if len(h.LogoutUrl) > 0 {
return c.Redirect(302, h.LogoutUrl)
if conf.LogoutUrl != "" {
return c.Redirect(302, conf.LogoutUrl)
}
// or standard url
return c.Redirect(302, "../")
@ -108,12 +116,27 @@ func main() {
// health-check
e.GET("/ping", func(c echo.Context) error {
u := &handler.Error{
u := &webserver.Error{
Message: "OK",
}
return c.JSON(http.StatusOK, u)
})
// Start DNS Server
dnsServer := &dnsserver.Server{
Host: "",
Port: 53,
RTimeout: 5 * time.Second,
WTimeout: 5 * time.Second,
}
handler := &dnsserver.Handler{
Config: conf,
DB: dbConn,
}
dnsServer.Run(handler)
// Start server
e.Logger.Fatal(e.Start(":8080"))
}

View file

@ -1,95 +0,0 @@
package nswrapper
import (
"bufio"
"bytes"
"fmt"
"github.com/labstack/gommon/log"
"io/ioutil"
"os"
"os/exec"
)
// UpdateRecord builds a nsupdate file and updates a record by executing it with nsupdate.
func UpdateRecord(hostname string, target string, addrType string, zone string, ttl int, enableWildcard bool) error {
log.Info(fmt.Sprintf("%s record update request: %s -> %s", addrType, hostname, target))
f, err := ioutil.TempFile(os.TempDir(), "dyndns")
if err != nil {
return err
}
defer os.Remove(f.Name())
w := bufio.NewWriter(f)
w.WriteString(fmt.Sprintf("server %s\n", "localhost"))
w.WriteString(fmt.Sprintf("zone %s\n", zone))
w.WriteString(fmt.Sprintf("update delete %s.%s %s\n", hostname, zone, addrType))
if enableWildcard {
w.WriteString(fmt.Sprintf("update delete %s.%s %s\n", "*."+hostname, zone, addrType))
}
w.WriteString(fmt.Sprintf("update add %s.%s %v %s %s\n", hostname, zone, ttl, addrType, target))
if enableWildcard {
w.WriteString(fmt.Sprintf("update add %s.%s %v %s %s\n", "*."+hostname, zone, ttl, addrType, target))
}
w.WriteString("send\n")
w.Flush()
f.Close()
cmd := exec.Command("/usr/bin/nsupdate", f.Name())
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
return fmt.Errorf("%v: %v", err, stderr.String())
}
if out.String() != "" {
return fmt.Errorf(out.String())
}
return nil
}
// DeleteRecord builds a nsupdate file and deletes a record by executing it with nsupdate.
func DeleteRecord(hostname string, zone string, enableWildcard bool) error {
fmt.Printf("record delete request: %s\n", hostname)
f, err := ioutil.TempFile(os.TempDir(), "dyndns")
if err != nil {
return err
}
defer os.Remove(f.Name())
w := bufio.NewWriter(f)
w.WriteString(fmt.Sprintf("server %s\n", "localhost"))
w.WriteString(fmt.Sprintf("zone %s\n", zone))
w.WriteString(fmt.Sprintf("update delete %s.%s\n", hostname, zone))
if enableWildcard {
w.WriteString(fmt.Sprintf("update delete %s.%s\n", "*."+hostname, zone))
}
w.WriteString("send\n")
w.Flush()
f.Close()
cmd := exec.Command("/usr/bin/nsupdate", f.Name())
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
return fmt.Errorf("%v: %v", err, stderr.String())
}
if out.String() != "" {
return fmt.Errorf(out.String())
}
return nil
}

View file

@ -34,14 +34,11 @@
<div class="btn-group" id="host_{{.ID}}" >
<button id="{{.ID}}" class="editHost btn btn-outline-secondary btn-sm"><img
src="/static/icons/pencil.svg" alt="" width="16" height="16" title="Edit"></button>&nbsp;
<button
id="{{.ID}}" class="deleteHost btn btn-outline-secondary btn-sm"><img
src="/static/icons/trash.svg"
alt="" width="16" height="16"
title="Delete"></button> &nbsp;
src="/static/icons/pencil.svg" alt="" width="16" height="16" title="Edit"></button>
<button id="{{.ID}}" class="deleteHost btn btn-outline-secondary btn-sm"><img
src="/static/icons/trash.svg" alt="" width="16" height="16" title="Delete"></button>
<button id="{{.ID}}" class="showHostLog btn btn-outline-secondary btn-sm"><img
src="/static/icons/table.svg" alt="" width="16" height="16" title="Logs"></button> &nbsp;
src="/static/icons/table.svg" alt="" width="16" height="16" title="Logs"></button>
<button id="{{.ID}}" class="copyUrlToClipboard btn btn-outline-secondary btn-sm"><img
src="/static/icons/clipboard.svg" alt="" width="16" height="16" title="Copy URL to clipboard"></button>
</div>

View file

@ -1,14 +1,12 @@
package handler
package webserver
import (
"fmt"
"net/http"
"strconv"
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
"github.com/benjaminbear/docker-ddns-server/dyndns/nswrapper"
"github.com/jinzhu/gorm"
"github.com/benjaminbear/docker-ddns-server/dyndns/db"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
// ListCNames fetches all cnames from database and lists them on the website.
@ -17,14 +15,14 @@ func (h *Handler) ListCNames(c echo.Context) (err error) {
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
}
cnames := new([]model.CName)
cnames := new([]db.CName)
if err = h.DB.Preload("Target").Find(cnames).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
return c.Render(http.StatusOK, "listcnames", echo.Map{
"cnames": cnames,
"title": h.Title,
"title": h.Config.Title,
})
}
@ -35,7 +33,7 @@ func (h *Handler) AddCName(c echo.Context) (err error) {
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
}
hosts := new([]model.Host)
hosts := new([]db.Host)
if err = h.DB.Find(hosts).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -43,7 +41,7 @@ func (h *Handler) AddCName(c echo.Context) (err error) {
return c.Render(http.StatusOK, "addcname", echo.Map{
"config": h.Config,
"hosts": hosts,
"title": h.Title,
"title": h.Config.Title,
})
}
@ -55,12 +53,12 @@ func (h *Handler) CreateCName(c echo.Context) (err error) {
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
}
cname := &model.CName{}
cname := &db.CName{}
if err = c.Bind(cname); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
host := &model.Host{}
host := &db.Host{}
if err = h.DB.First(host, c.FormValue("target_id")).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -79,10 +77,6 @@ func (h *Handler) CreateCName(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
if err = nswrapper.UpdateRecord(cname.Hostname, fmt.Sprintf("%s.%s", cname.Target.Hostname, cname.Target.Domain), "CNAME", cname.Target.Domain, cname.Ttl, h.AllowWildcard); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
return c.JSON(http.StatusOK, cname)
}
@ -98,7 +92,7 @@ func (h *Handler) DeleteCName(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
cname := &model.CName{}
cname := &db.CName{}
if err = h.DB.Preload("Target").First(cname, id).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -114,9 +108,5 @@ func (h *Handler) DeleteCName(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
if err = nswrapper.DeleteRecord(cname.Hostname, cname.Target.Domain, h.AllowWildcard); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
return c.JSON(http.StatusOK, id)
}

117
dyndns/webserver/handler.go Normal file
View file

@ -0,0 +1,117 @@
package webserver
import (
"github.com/benjaminbear/docker-ddns-server/dyndns/config"
"github.com/labstack/gommon/log"
"strings"
"time"
"github.com/benjaminbear/docker-ddns-server/dyndns/db"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/tg123/go-htpasswd"
"gorm.io/gorm"
)
type Handler struct {
DB *gorm.DB
AuthAdmin bool
Config *config.Config
LastClearedLogs time.Time
}
type CustomValidator struct {
Validator *validator.Validate
}
// Validate implements the Validator.
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.Validator.Struct(i)
}
type Error struct {
Message string `json:"message"`
}
func New(config *config.Config, db *gorm.DB) *Handler {
h := &Handler{Config: config, DB: db}
if config.AdminLogin == "" {
h.AuthAdmin = true
}
return h
}
// Authenticate is the method the website admin user and the host update user have to authenticate against.
// To gather admin rights the username password combination must match with the credentials given by the env var.
func (h *Handler) AuthenticateUpdate(username, password string, c echo.Context) (bool, error) {
h.CheckClearInterval()
reqParameter := c.QueryParam("hostname")
reqArr := strings.SplitN(reqParameter, ".", 2)
if len(reqArr) != 2 {
log.Error("Error: Something wrong with the hostname parameter")
return false, nil
}
host := &db.Host{}
if err := h.DB.Where(&db.Host{UserName: username, Password: password, Hostname: reqArr[0], Domain: reqArr[1]}).First(host).Error; err != nil {
log.Error("Error: ", err)
return false, nil
}
if host.ID == 0 {
log.Error("hostname or user user credentials unknown")
return false, nil
}
c.Set("updateHost", host)
return true, nil
}
func (h *Handler) AuthenticateAdmin(username, password string, c echo.Context) (bool, error) {
h.AuthAdmin = false
ok, err := h.authByEnv(username, password)
if err != nil {
log.Error("Error:", err)
return false, nil
}
if ok {
h.AuthAdmin = true
return true, nil
}
return false, nil
}
func (h *Handler) authByEnv(username, password string) (bool, error) {
hashReader := strings.NewReader(h.Config.AdminLogin)
pw, err := htpasswd.NewFromReader(hashReader, htpasswd.DefaultSystems, nil)
if err != nil {
return false, err
}
if ok := pw.Match(username, password); ok {
return true, nil
}
return false, nil
}
// Check if a log cleaning is needed
func (h *Handler) CheckClearInterval() {
if h.Config.ClearLogInterval > 0 {
if !equalDate(time.Now(), h.LastClearedLogs) {
go h.ClearLogs()
}
}
}
// compare two dates
func equalDate(date1, date2 time.Time) bool {
y1, m1, d1 := date1.Date()
y2, m2, d2 := date2.Date()
return y1 == y2 && m1 == m2 && d1 == d2
}

View file

@ -1,18 +1,19 @@
package handler
package webserver
import (
"fmt"
l "github.com/labstack/gommon/log"
"net"
"net/http"
"strconv"
"time"
l "github.com/labstack/gommon/log"
"github.com/benjaminbear/docker-ddns-server/dyndns/nswrapper"
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
"github.com/jinzhu/gorm"
"github.com/benjaminbear/docker-ddns-server/dyndns/db"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
const (
@ -30,7 +31,7 @@ func (h *Handler) GetHost(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
host := &model.Host{}
host := &db.Host{}
if err = h.DB.First(host, id).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -45,14 +46,14 @@ func (h *Handler) ListHosts(c echo.Context) (err error) {
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
}
hosts := new([]model.Host)
hosts := new([]db.Host)
if err = h.DB.Find(hosts).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
return c.Render(http.StatusOK, "listhosts", echo.Map{
"hosts": hosts,
"title": h.Title,
"title": h.Config.Title,
})
}
@ -65,7 +66,7 @@ func (h *Handler) AddHost(c echo.Context) (err error) {
return c.Render(http.StatusOK, "edithost", echo.Map{
"addEdit": "add",
"config": h.Config,
"title": h.Title,
"title": h.Config.Title,
})
}
@ -80,7 +81,7 @@ func (h *Handler) EditHost(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
host := &model.Host{}
host := &db.Host{}
if err = h.DB.First(host, id).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -89,7 +90,7 @@ func (h *Handler) EditHost(c echo.Context) (err error) {
"host": host,
"addEdit": "edit",
"config": h.Config,
"title": h.Title,
"title": h.Config.Title,
})
}
@ -101,7 +102,7 @@ func (h *Handler) CreateHost(c echo.Context) (err error) {
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
}
host := &model.Host{}
host := &db.Host{}
if err = c.Bind(host); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -118,18 +119,6 @@ func (h *Handler) CreateHost(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
// If a ip is set create dns entry
if host.Ip != "" {
ipType := nswrapper.GetIPType(host.Ip)
if ipType == "" {
return c.JSON(http.StatusBadRequest, &Error{fmt.Sprintf("ip %s is not a valid ip", host.Ip)})
}
if err = nswrapper.UpdateRecord(host.Hostname, host.Ip, ipType, host.Domain, host.Ttl, h.AllowWildcard); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
}
return c.JSON(http.StatusOK, host)
}
@ -141,7 +130,7 @@ func (h *Handler) UpdateHost(c echo.Context) (err error) {
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
}
hostUpdate := &model.Host{}
hostUpdate := &db.Host{}
if err = c.Bind(hostUpdate); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -151,12 +140,11 @@ func (h *Handler) UpdateHost(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
host := &model.Host{}
host := &db.Host{}
if err = h.DB.First(host, id).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
forceRecordUpdate := host.UpdateHost(hostUpdate)
if err = c.Validate(host); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -165,18 +153,6 @@ func (h *Handler) UpdateHost(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
// If ip or ttl changed update dns entry
if forceRecordUpdate {
ipType := nswrapper.GetIPType(host.Ip)
if ipType == "" {
return c.JSON(http.StatusBadRequest, &Error{fmt.Sprintf("ip %s is not a valid ip", host.Ip)})
}
if err = nswrapper.UpdateRecord(host.Hostname, host.Ip, ipType, host.Domain, host.Ttl, h.AllowWildcard); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
}
return c.JSON(http.StatusOK, host)
}
@ -192,7 +168,7 @@ func (h *Handler) DeleteHost(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
host := &model.Host{}
host := &db.Host{}
if err = h.DB.First(host, id).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -202,11 +178,11 @@ func (h *Handler) DeleteHost(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
if err = tx.Where(&model.Log{HostID: uint(id)}).Delete(&model.Log{}).Error; err != nil {
if err = tx.Where(&db.Log{HostID: uint(id)}).Delete(&db.Log{}).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
if err = tx.Where(&model.CName{TargetID: uint(id)}).Delete(&model.CName{}).Error; err != nil {
if err = tx.Where(&db.CName{TargetID: uint(id)}).Delete(&db.CName{}).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@ -216,10 +192,6 @@ func (h *Handler) DeleteHost(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
if err = nswrapper.DeleteRecord(host.Hostname, host.Domain, h.AllowWildcard); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
return c.JSON(http.StatusOK, id)
}
@ -227,12 +199,12 @@ func (h *Handler) DeleteHost(c echo.Context) (err error) {
// Hostname, IP and senders IP are validated, a log entry is created
// and finally if everything is ok, the DNS Server will be updated
func (h *Handler) UpdateIP(c echo.Context) (err error) {
host, ok := c.Get("updateHost").(*model.Host)
host, ok := c.Get("updateHost").(*db.Host)
if !ok {
return c.String(http.StatusBadRequest, "badauth\n")
}
log := &model.Log{Status: false, Host: *host, TimeStamp: time.Now(), UserAgent: nswrapper.ShrinkUserAgent(c.Request().UserAgent())}
log := &db.Log{Status: false, Host: *host, TimeStamp: time.Now(), UserAgent: nswrapper.ShrinkUserAgent(c.Request().UserAgent())}
log.SentIP = c.QueryParam(("myip"))
// Get caller IP
@ -275,16 +247,6 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) {
}
}
// add/update DNS record
if err = nswrapper.UpdateRecord(log.Host.Hostname, log.SentIP, ipType, log.Host.Domain, log.Host.Ttl, h.AllowWildcard); err != nil {
log.Message = fmt.Sprintf("DNS error: %v", err)
l.Error(log.Message)
if err = h.CreateLogEntry(log); err != nil {
l.Error(err)
}
return c.String(http.StatusBadRequest, "dnserr\n")
}
log.Host.Ip = log.SentIP
log.Host.LastUpdate = log.TimeStamp
log.Status = true
@ -297,8 +259,8 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) {
}
func (h *Handler) checkUniqueHostname(hostname, domain string) error {
hosts := new([]model.Host)
if err := h.DB.Where(&model.Host{Hostname: hostname, Domain: domain}).Find(hosts).Error; err != nil {
hosts := new([]db.Host)
if err := h.DB.Where(&db.Host{Hostname: hostname, Domain: domain}).Find(hosts).Error; err != nil {
return err
}
@ -306,8 +268,8 @@ func (h *Handler) checkUniqueHostname(hostname, domain string) error {
return fmt.Errorf("hostname already exists")
}
cnames := new([]model.CName)
if err := h.DB.Preload("Target").Where(&model.CName{Hostname: hostname}).Find(cnames).Error; err != nil {
cnames := new([]db.CName)
if err := h.DB.Preload("Target").Where(&db.CName{Hostname: hostname}).Find(cnames).Error; err != nil {
return err
}

View file

@ -1,17 +1,18 @@
package handler
package webserver
import (
"fmt"
"log"
"net/http"
"strconv"
"time"
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
"github.com/benjaminbear/docker-ddns-server/dyndns/db"
"github.com/labstack/echo/v4"
)
// CreateLogEntry simply adds a log entry to the database.
func (h *Handler) CreateLogEntry(log *model.Log) (err error) {
func (h *Handler) CreateLogEntry(log *db.Log) (err error) {
if err = h.DB.Create(log).Error; err != nil {
return err
}
@ -25,14 +26,14 @@ func (h *Handler) ShowLogs(c echo.Context) (err error) {
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
}
logs := new([]model.Log)
logs := new([]db.Log)
if err = h.DB.Preload("Host").Limit(30).Order("created_at desc").Find(logs).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
return c.Render(http.StatusOK, "listlogs", echo.Map{
"logs": logs,
"title": h.Title,
"title": h.Config.Title,
})
}
@ -47,19 +48,19 @@ func (h *Handler) ShowHostLogs(c echo.Context) (err error) {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
logs := new([]model.Log)
if err = h.DB.Preload("Host").Where(&model.Log{HostID: uint(id)}).Order("created_at desc").Limit(30).Find(logs).Error; err != nil {
logs := new([]db.Log)
if err = h.DB.Preload("Host").Where(&db.Log{HostID: uint(id)}).Order("created_at desc").Limit(30).Find(logs).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
return c.Render(http.StatusOK, "listlogs", echo.Map{
"logs": logs,
"title": h.Title,
"title": h.Config.Title,
})
}
func (h *Handler) ClearLogs() {
var clearInterval = strconv.FormatUint(h.ClearInterval, 10) + " day"
clearInterval := fmt.Sprintf("%d day", h.Config.ClearLogInterval)
h.DB.Exec("DELETE FROM LOGS WHERE created_at < datetime('now', '-" + clearInterval + "');REINDEX LOGS;")
h.LastClearedLogs = time.Now()
log.Print("logs cleared")