initial commit

This commit is contained in:
2021-02-05 23:57:51 +01:00
parent 7eaf2e717f
commit 64c307cb7b
15 changed files with 1182 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

9
.idea/ddnsService.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ddnsService.iml" filepath="$PROJECT_DIR$/.idea/ddnsService.iml" />
</modules>
</component>
</project>

323
HttpServer.go Normal file
View File

@@ -0,0 +1,323 @@
package main
import (
"encoding/json"
"fmt"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"net"
"net/http"
)
type HttpServer struct {
_server *http.Server
_database *Database
_oauth *OAuth2
_renderer *Renderer
_dnsclient *DnsUpdate
_ipAddresses map[string][]string
}
type GuiSetting struct {
FilterString *string
}
func getIP(r *http.Request) (string, error) {
var host string
forwarded := r.Header.Get("X-FORWARDED-FOR")
if forwarded != "" {
host = forwarded
} else {
host = r.RemoteAddr
}
host,_ , err := net.SplitHostPort(host)
if err != nil {
return "", err
}
return host, nil
}
func (h *HttpServer) doaction(w http.ResponseWriter, r *http.Request, params *AdminPageParams){
if action := r.URL.Query().Get("action"); action != "" {
switch action {
case "update":
host := r.URL.Query().Get("host")
ip := r.URL.Query().Get("ip")
if !h._database.ExistHost(host) {
params.Alerts = append(params.Alerts, Alert{Type:"error", Message: fmt.Sprintf("host %s does not exist", host)})
break
}
err := h._dnsclient.Update(host, ip)
if err != nil {
params.Alerts = append(params.Alerts, Alert{Type:"error", Message: err.Error()})
} else {
params.Alerts = append(params.Alerts, Alert{Type:"success", Message: "successfully updated host"})
}
break
case "delete":
host := r.URL.Query().Get("host")
log.Info("deleting host " + host)
h._database.DeleteHost(host)
h._dnsclient.Remove(host)
delete(h._ipAddresses, host)
break
case "externalresolve":
host := r.URL.Query().Get("host")
ips, err := h._dnsclient.ExternalResolve(host)
if err != nil {
params.Alerts = append(params.Alerts, Alert{
Type: "error",
Message: fmt.Sprintf("host %s could not be resolved", host),
})
} else {
params.Alerts = append(params.Alerts, Alert{
Type: "success",
Message: fmt.Sprintf("host %s resolved to following addresses:\n %s", host, fmt.Sprint(ips)),
})
}
case "resolve":
host := r.URL.Query().Get("host")
ipAddresses, err := h._dnsclient.Resolve(host)
if err == nil && len(ipAddresses) > 0 {
params.Alerts = append(params.Alerts, Alert{
Type: "success",
Message: fmt.Sprintf("host %s resolved to following addresses:\n %s", host, fmt.Sprint(ipAddresses)),
})
h._ipAddresses[host] = ipAddresses
} else {
params.Alerts = append(params.Alerts, Alert{
Type: "error",
Message: fmt.Sprintf("host %s could not be resolved", host),
})
delete(h._ipAddresses, host)
}
case "add":
host := r.URL.Query().Get("host")
_, err := h._database.CreateHost(host)
if err == nil {
params.Alerts = append(params.Alerts, Alert{
Type: "success",
Message: fmt.Sprintf("created host %s", host),
})
} else {
params.Alerts = append(params.Alerts, Alert{
Type: "error",
Message: fmt.Sprintf("could not create host %s.\n reason: %s", host, err.Error()),
})
}
break
}
} else {
}
}
func (h *HttpServer) doguisetting(w http.ResponseWriter, r *http.Request) *GuiSetting{
settings := new(GuiSetting)
settings.FilterString = nil
if setting := r.URL.Query().Get("setting"); setting != "" {
switch setting {
case "filter":
host := r.URL.Query().Get("host")
settings.FilterString = &host
break
}
}
return settings
}
func (h *HttpServer) adminPage(w http.ResponseWriter, r *http.Request) {
token, ok := h._oauth.checkOAuth(w, r, true)
if !ok { return }
params := new(AdminPageParams)
params.Alerts = make([]Alert, 0)
params.Hosts = make(map[string]string)
params.IpAddresses = h._ipAddresses
h.doaction(w,r, params)
settings := h.doguisetting(w,r)
claims, _ := h._oauth.GetClaims(w, token)
params.Email = claims.Email
params.Profile = claims.Profile
if settings.FilterString != nil {
params.Hosts = h._database.GetExistingHosts(*settings.FilterString)
params.Alerts = append(
params.Alerts,
Alert{
Type: "info",
Message: fmt.Sprintf("returned %d hosts", len(params.Hosts)),
})
} else {
params.Hosts = h._database.GetExistingHosts("")
}
h._renderer.RenderAdminPage(w, params)
}
func (h *HttpServer) registerHandler(w http.ResponseWriter, r *http.Request) {
if _, ok := h._oauth.checkOAuth(w, r, false); ok {
return
}
host := r.URL.Query().Get("host")
clientip, err := getIP(r)
if err != nil { log.Fatal(err) }
if h._database.IsBannedHost(clientip) {
h._database.IncrementBanHost(clientip)
log.Error("host ", clientip, " is banned!")
w.WriteHeader(http.StatusForbidden)
return
}
if h._database.ExistHost(host) {
h._database.IncrementBanHost(clientip)
w.WriteHeader(http.StatusNotAcceptable)
return
}
token, err := h._database.CreateHost(host)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
ret := map[string]string {
"host": host,
"token": token,
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(ret)
}
func (h *HttpServer) updateHandler(w http.ResponseWriter, r *http.Request) {
hosts := r.URL.Query()["host"]
tokens := r.URL.Query()["token"]
myips := r.URL.Query()["myip"]
var myip string
clientip, err := getIP(r)
if h._database.IsBannedHost(clientip) {
h._database.IncrementBanHost(clientip)
log.Error("host ", clientip, " is banned!")
w.WriteHeader(http.StatusForbidden)
return
}
if err != nil {
log.Fatal(err)
}
if len(hosts) == 0 {
h._database.IncrementBanHost(clientip)
w.WriteHeader(http.StatusNotAcceptable)
return
}
if len(tokens) == 0 {
h._database.IncrementBanHost(clientip)
w.WriteHeader(http.StatusNotAcceptable)
return
}
if len(myips) == 0 {
myip = clientip
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
} else {
myip = myips[0]
}
host := hosts[0]
token := tokens[0]
if !h._database.Authorize(host, token) {
h._database.IncrementBanHost(clientip)
w.WriteHeader(http.StatusForbidden)
return
}
log.Info("authorization successful.")
log.Info("will update host ", host, " ip ", myip)
w.WriteHeader(http.StatusOK)
}
func (h *HttpServer) Listen() {
h._server.ListenAndServe()
}
func CreateHttpServer(config *Config) *HttpServer {
var httpserver *HttpServer
var err error
httpserver = new(HttpServer)
r := mux.NewRouter()
httpserver._ipAddresses = make(map[string][]string)
httpserver._renderer, err = CreateRenderer(config)
if err != nil {
log.Fatal(err)
}
httpserver._database, err = CreateDatabase(config)
if err != nil {
log.Fatal(err)
}
httpserver._oauth, err = CreateOAuth2(config)
if err != nil {
log.Fatal(err)
}
httpserver._dnsclient = NewTestInstance()
httpserver._server = &http.Server{
Addr: config.Addr,
Handler: r,
}
r.HandleFunc("/register", httpserver.registerHandler)
r.HandleFunc("/update", httpserver.updateHandler)
r.HandleFunc("/admin", httpserver.adminPage)
return httpserver
}

14
config.go Normal file
View File

@@ -0,0 +1,14 @@
package main
type Config struct {
Addr string
DataDir string
OAuth2Enable bool
OAuth2RedirectUrl string
OAuth2ClientID string
OAuth2ClientSecret string
IssuerUrl string
ZoneUrl string
}

162
database.go Normal file
View File

@@ -0,0 +1,162 @@
package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"errors"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"math"
"math/big"
"os"
"path"
"strings"
"time"
"github.com/patrickmn/go-cache"
)
type Database struct {
_db *leveldb.DB
_cache *cache.Cache
_zone string
}
func (d *Database) ExistHost(host string) bool {
ret, err := d._db.Has([]byte("hosts/" + host), nil)
if err != nil {
log.Fatal(err)
}
return ret
}
func (d *Database) DeleteHost(host string) bool {
err := d._db.Delete([]byte("hosts/" + host), nil)
return err == nil
}
func (d *Database) GetExistingHosts(match string) map[string]string {
iter := d._db.NewIterator(util.BytesPrefix([]byte("hosts/" + match)), nil)
ret := make(map[string]string)
for iter.Next() {
hostname := strings.TrimPrefix(string(iter.Key()), "hosts/")
ret[hostname] = string(iter.Value())
}
return ret
}
func (d *Database) Authorize(host string, token_user string) bool {
token, err := d._db.Get([]byte("hosts/" + host), nil)
if err != nil {
return false
}
if len(token) < sha512.Size {
return false
}
if len(token_user) < sha512.Size {
return false
}
if string(token) == token_user {
return true
}
log.Error(string(token), "!=", token_user)
return false
}
func generateToken() string {
maxInt := big.NewInt(math.MaxInt64)
randInt, err := rand.Int(rand.Reader, maxInt)
if err != nil {
log.Fatal(err)
}
h_plain := []byte(randInt.String() + time.Now().String())
h:= sha256.Sum256(h_plain)
for i:=0;i<10000; i++ {
h = sha256.Sum256(h[:])
}
var buf bytes.Buffer
base64.NewEncoder(base64.URLEncoding, &buf).Write(h[:])
return buf.String()
}
func (d *Database) IsBannedHost(host string) bool {
val, ok := d._cache.Get(host)
var counter int
if ok {
counter = val.(int)
if counter > 3 {
return true
} else {
return false
}
} else {
return false
}
}
func (d *Database) IncrementBanHost(host string) {
_, ok := d._cache.Get(host)
if ok {
d._cache.IncrementInt(host,1)
} else {
d._cache.Add(host, 1, cache.DefaultExpiration)
}
}
func (d *Database) CreateHost(host string) (string, error) {
if d.ExistHost(host) {
return "", errors.New("host already existent")
}
if host == "" {
return "", errors.New("given hostname is empty")
}
if _, ok := dns.IsDomainName(host + "." + d._zone); !ok {
return "", errors.New("given hostname is invalid")
}
token := generateToken()
d._db.Put([]byte("hosts/" + host), []byte(token), nil)
return token, nil
}
func CreateDatabase(config *Config) (*Database, error){
_, err := os.Stat(config.DataDir)
if err != nil {
if os.IsNotExist(err) {
os.MkdirAll(config.DataDir, os.ModeDir)
}
}
file := path.Join(config.DataDir, "ddnsService.db")
store, err := leveldb.OpenFile(file, nil)
db := cache.New(5*time.Minute, 10*time.Minute)
if err != nil {
return nil, err
}
return &Database{_db: store, _cache: db}, nil
}

193
dns_update.go Normal file
View File

@@ -0,0 +1,193 @@
package main
import (
"context"
"errors"
"github.com/miekg/dns"
"net"
"time"
)
type DnsUpdate struct {
Timeout int
TSigKeyName string
TSigKey string
TSigAlgorithm string
DnsHost string
Zone string
}
func NewTestInstance() *DnsUpdate {
return &DnsUpdate{
DnsHost: "45.129.181.183:53",
Timeout: 300,
TSigKeyName: "ddns-key",
TSigAlgorithm: dns.HmacSHA512,
TSigKey: "rcDW6VdBFV8+LKH4JHXKRan0ZzoJBbbfeK6sL8VOSM60nt99Z/0jQbDp9PyF3q9TsmEBb0rTI2txySIH998Vfw==",
Zone: "hub.voglfrei.net",
}
}
func (d *DnsUpdate) createIp4Addr(host string, addr net.IP) *dns.A {
ip := addr.To4()
if ip == nil {
return nil
}
return &dns.A{
Hdr: dns.RR_Header{
Name: dns.Fqdn(host+"."+d.Zone),
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: uint32(d.Timeout),
},
A: ip,
}
}
func (d *DnsUpdate) createIp6Addr(host string, addr net.IP) *dns.AAAA {
ip := addr.To16()
if ip == nil {
return nil
}
return &dns.AAAA{
Hdr: dns.RR_Header{
Name: dns.Fqdn(host+"."+d.Zone),
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: uint32(d.Timeout),
},
AAAA: ip,
}
}
func (d *DnsUpdate) createRemovals(host string) []dns.RR {
return []dns.RR{
&dns.RR_Header{Name: dns.Fqdn(host+"."+d.Zone)},
}
}
func (d *DnsUpdate) createInserts(host string, addr net.IP) []dns.RR {
var rr = make([]dns.RR,0)
if r := d.createIp4Addr(host, addr); r != nil {
rr = append(rr, r)
}
if r := d.createIp6Addr(host, addr); r != nil {
rr = append(rr, r)
}
return rr
}
func (d *DnsUpdate) exchange(msg *dns.Msg) (*dns.Msg, error) {
var client = new(dns.Client)
timeout := time.Duration(d.Timeout * int(time.Second))
client.DialTimeout = timeout
client.ReadTimeout = timeout
client.WriteTimeout = timeout
client.TsigSecret = map[string]string {dns.Fqdn(d.TSigKeyName): d.TSigKey }
r, _, err := client.Exchange(msg, d.DnsHost)
if err != nil {
return nil, err
}
if r.Rcode == dns.RcodeSuccess {
return r, nil
} else {
return nil, errors.New(dns.RcodeToString[r.Rcode])
}
}
func (d *DnsUpdate) Remove(host string) error {
var msg = new(dns.Msg)
msg.SetUpdate(dns.Fqdn(d.Zone))
msg.RemoveName(d.createRemovals(host))
msg.SetTsig(dns.Fqdn(d.TSigKeyName), d.TSigAlgorithm, uint16(d.Timeout), time.Now().Unix())
_, err := d.exchange(msg)
return err
}
func (d *DnsUpdate) Resolve(host string) ([]string, error) {
var msg = new(dns.Msg)
msg.SetQuestion(dns.Fqdn(host + "." + d.Zone), dns.TypeA)
r, err := d.exchange(msg)
if err != nil {return nil, err}
hosts := make([]string, 0)
for _, v := range r.Answer {
if v.Header().Rrtype == dns.TypeA {
hosts = append(hosts, v.(*dns.A).A.To4().String())
} else if v.Header().Rrtype == dns.TypeAAAA {
hosts = append(hosts, v.(*dns.AAAA).AAAA.To16().String())
}
}
return hosts, nil
}
func (d *DnsUpdate) ExternalResolve(host string) ([]string, error ) {
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Millisecond * time.Duration(10000),
}
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
}
ip, err := r.LookupHost(context.Background(), host + "." + d.Zone)
if err != nil {
return nil, err
}
return ip, nil
}
func (d *DnsUpdate) Update(host string, addrs ...string) error {
var err error
err = d.Remove(host)
if err != nil {return err }
return d.AddMany(host, addrs...)
}
func (d *DnsUpdate) AddMany(host string, addrs ...string) error {
var err error
for _, v := range addrs {
err = d.Add(host, v)
if err != nil {return err }
}
return nil
}
func (d *DnsUpdate) Add(host string, addr string) error {
ip, err := net.ResolveIPAddr("ip", addr)
if err != nil {
return err
}
var msg = new(dns.Msg)
msg.SetUpdate(dns.Fqdn(d.Zone))
msg.Insert(d.createInserts(host, ip.IP))
msg.SetTsig(dns.Fqdn(d.TSigKeyName), d.TSigAlgorithm, uint16(d.Timeout), time.Now().Unix())
_, err = d.exchange(msg)
return err
}

13
go.mod Normal file
View File

@@ -0,0 +1,13 @@
module ddnsService
go 1.15
require (
github.com/coreos/go-oidc/v3 v3.0.0
github.com/gorilla/mux v1.8.0
github.com/miekg/dns v1.1.35
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sirupsen/logrus v1.7.0
github.com/syndtr/goleveldb v1.0.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
)

87
go.sum Normal file
View File

@@ -0,0 +1,87 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ=
github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
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/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs=
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
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/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c h1:zJ0mtu4jCalhKg6Oaukv6iIkb+cOvDrajDH9DH46Q4M=
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

14
main.go Normal file
View File

@@ -0,0 +1,14 @@
package main
func main() {
config := Config{
DataDir: "/var/lib/ddnsService",
Addr: ":8999", OAuth2ClientID: "0oa472q93Zu227hXY5d6",
OAuth2ClientSecret: "sj98SdfZxjkYxUd8NCbyhSuPX1D7kPARDRJ0kVUe",
IssuerUrl: "https://dev-1614211.okta.com/oauth2/default",
OAuth2RedirectUrl: "http://localhost:8999/admin",
ZoneUrl: "hub.voglfrei.net"}
server := CreateHttpServer(&config)
server.Listen()
}

142
oauth2.go Normal file
View File

@@ -0,0 +1,142 @@
package main
import (
"context"
"encoding/base64"
"fmt"
"github.com/coreos/go-oidc/v3/oidc"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"net/http"
)
type OidcClams struct {
Email string `json:"email"`
Profile string `json:"profile"`
}
type OAuth2 struct {
_ctx context.Context
_oauth2Config *oauth2.Config
_oidcVerifier *oidc.IDTokenVerifier
_oidcProvider *oidc.Provider
_nonces map[string]string
}
func (o *OAuth2) GetClaims(w http.ResponseWriter, token *oidc.IDToken) (*OidcClams, error) {
var claims = new(OidcClams)
err := token.Claims(&claims)
if err != nil {
return nil, err
}
return claims, nil
}
func (o *OAuth2) checkOAuth(w http.ResponseWriter, r *http.Request, allowRedirect bool) (*oidc.IDToken, bool) {
cookie, err := r.Cookie("id_token")
if err == nil {
log.Info("got id_token cookie")
token, err := o._oidcVerifier.Verify(o._ctx, cookie.Value)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return nil, false
}
dst, _ := base64.URLEncoding.DecodeString(cookie.Value)
fmt.Printf(string(dst))
return token, true
} else {
//no Bearer available
if code := r.URL.Query().Get("code") ; code != "" {
state := r.URL.Query().Get("state")
//check if nonce is known by server
if _, ok := o._nonces[state]; ok == false {
return nil, false
}
delete(o._nonces, state)
oauth2Token, err := o._oauth2Config.Exchange(o._ctx, code)
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return nil, false
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
return nil, false
}
log.Info(rawIDToken)
token, err2 := o._oidcVerifier.Verify(o._ctx, rawIDToken)
if err2 != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return nil, false
}
idCookie := http.Cookie{
Name: "id_token",
Value: rawIDToken,
Domain: "",
Expires: token.Expiry,
Secure: false,
HttpOnly: true,
Path: "/",
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, &idCookie)
return token, true
} else {
//no auth code and no bearer -> redirect
if allowRedirect {
nonce := generateToken()
o._nonces[nonce] = nonce
http.Redirect(w, r, o._oauth2Config.AuthCodeURL(nonce), http.StatusFound)
}
return nil, false
}
}
}
func CreateOAuth2(config *Config) (*OAuth2, error) {
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, config.IssuerUrl)
if err != nil {
return nil, err
}
oidcConfig := &oidc.Config{
ClientID: config.OAuth2ClientID,
}
// Configure an OpenID Connect aware OAuth2 client.
verifier := provider.Verifier(oidcConfig)
oauthConfig := oauth2.Config{
ClientID: config.OAuth2ClientID,
ClientSecret: config.OAuth2ClientSecret,
Endpoint: provider.Endpoint(),
RedirectURL: config.OAuth2RedirectUrl,
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
return &OAuth2{
_ctx: ctx,
_oauth2Config: &oauthConfig,
_oidcVerifier: verifier,
_oidcProvider: provider,
_nonces: make(map[string]string),
}, nil
}

56
renderer.go Normal file
View File

@@ -0,0 +1,56 @@
package main
import (
"html/template"
"net/http"
)
type AdminPageParams struct {
Hosts map[string]string
Email string
Profile string
Alerts []Alert
IpAddresses map[string][]string
}
type Alert struct {
Type string
Message string
}
type Renderer struct {
_template *template.Template
}
func (r *Renderer) RenderAdminPage(w http.ResponseWriter, params *AdminPageParams) {
r.renderPage(w, "adminpage.html", params)
}
func (r *Renderer) renderPage(w http.ResponseWriter, templateName string, params interface{}) {
err := r._template.ExecuteTemplate(w,templateName, params)
if err != nil {
text := "error in rendering template " + templateName + "\n" + err.Error()
http.Error(w, text , http.StatusInternalServerError)
}
}
func CreateRenderer(config *Config) (*Renderer, error) {
renderer := new(Renderer)
var err error
renderer._template, err = template.ParseGlob("resources/templates/*")
if err != nil {
return nil, err
}
return renderer, nil
}

View File

@@ -0,0 +1,117 @@
{{template "header"}}
<div class="container">
<div class="container">
<div class="row">
<div class="col-md-4">
</div>
<div class="col-md-4">
</div>
<div class="col-md-4 card">
<div class="container">
<div class="row">
<div class="col-8">
<h5 class="card-title">Logged in as {{.Email}}</h5>
</div>
<div class="col-4">
<a class="btn btn-outline-primary" href="#">Logout</a>
</div>
<p class="card-text">{{.Profile}}</p>
</div>
</div>
</div>
</div>
</div>
<br/>
<div class="container">
{{range .Alerts}}
{{if eq .Type "success"}}
<div class="alert alert-success" role="alert">
<strong>Success</strong> {{.Message}}
</div>
{{else if eq .Type "warning"}}
<div class="alert alert-warning" role="alert">
<strong>Warning</strong> {{.Message}}
</div>
{{else if eq .Type "error"}}
<div class="alert alert-danger" role="alert">
<strong>Error</strong> {{.Message}}
</div>
{{else if eq .Type "info"}}
<div class="alert alert-primary" role="alert">
<strong>Info</strong> {{.Message}}
</div>
{{end}}
{{end}}
</div>
<div class="container">
<div class="row">
<div class="col-md-4 card">
<h5 class="card-title">add new host</h5>
<form class="card-body" action="/admin" method="get" target="_self">
<label for="fname">host name:</label>
<input type="text" id="fname" name="host">
<input type="submit" value="Submit">
<input type="hidden" id="action" name="action" value="add">
</form>
</div>
<div class="col-md-4 card">
<h5 class="card-title">add update ip Address</h5>
<form class="card-body" action="/admin" method="get" target="_self">
<label for="fhost">host name:</label>
<input type="text" id="fhost" name="host">
<label for="fip">ip address:</label>
<input type="text" id="fip" name="ip">
<input type="submit" value="Submit">
<input type="hidden" id="action" name="action" value="update">
</form>
</div>
<div class="col-md-4 card">
<h5 class="card-title">filter available hosts</h5>
<form class="card-body" action="/admin" method="get" target="_self">
<label for="fname">filter (leave empty for all):</label>
<input type="text" id="fname" name="host">
<input type="submit" value="Submit">
<input type="hidden" id="action" name="setting" value="filter">
</form>
</div>
</div>
</div>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Hostname</th>
<th scope="col">Token</th>
<th scope="col">IP Addresses</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{{range $key, $val := .Hosts }}
<tr>
<td>{{$key}}</td>
<td>{{$val}}</td>
<td>
{{range index $.IpAddresses $key}}
{{.}}<br/>
{{end}}
</td>
<td>
<a href="/admin?action=delete&host={{$key}}" class="btn btn-outline-danger">delete</a>
<a href="/admin?action=resolve&host={{$key}}" class="btn btn-outline-primary">resolve</a>
<a href="/admin?action=externalresolve&host={{$key}}" class="btn btn-outline-info">external resolve (google)</a>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "footer"}}

View File

@@ -0,0 +1,4 @@
{{define "footer"}}
</body>
</html>
{{end}}

View File

@@ -0,0 +1,32 @@
{{define "header"}}
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<title>ddnsService</title>
</head>
<body>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script
src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script>
$( document ).ready(function() {
window.history.pushState({}, document.title, "/admin");
$(".alert").hide().fadeIn(200).delay(2000).fadeOut(1000, function () { $(this).remove(); });
});
</script>
{{end}}