diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..73f69e0
--- /dev/null
+++ b/.idea/.gitignore
@@ -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/
diff --git a/.idea/ddnsService.iml b/.idea/ddnsService.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/ddnsService.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..d82ff13
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HttpServer.go b/HttpServer.go
new file mode 100644
index 0000000..fa3a08c
--- /dev/null
+++ b/HttpServer.go
@@ -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
+}
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..bb22008
--- /dev/null
+++ b/config.go
@@ -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
+
+}
diff --git a/database.go b/database.go
new file mode 100644
index 0000000..0a8634a
--- /dev/null
+++ b/database.go
@@ -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
+
+}
diff --git a/dns_update.go b/dns_update.go
new file mode 100644
index 0000000..447ee85
--- /dev/null
+++ b/dns_update.go
@@ -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
+
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..efe4f18
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..0c6c206
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..4ac016a
--- /dev/null
+++ b/main.go
@@ -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()
+}
diff --git a/oauth2.go b/oauth2.go
new file mode 100644
index 0000000..5230e4e
--- /dev/null
+++ b/oauth2.go
@@ -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
+}
\ No newline at end of file
diff --git a/renderer.go b/renderer.go
new file mode 100644
index 0000000..df6cd0e
--- /dev/null
+++ b/renderer.go
@@ -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
+}
+
+
diff --git a/resources/templates/adminpage.html b/resources/templates/adminpage.html
new file mode 100644
index 0000000..1625358
--- /dev/null
+++ b/resources/templates/adminpage.html
@@ -0,0 +1,117 @@
+{{template "header"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Logged in as {{.Email}}
+
+
+
{{.Profile}}
+
+
+
+
+
+
+
+
+
+
+ {{range .Alerts}}
+ {{if eq .Type "success"}}
+
+ Success {{.Message}}
+
+ {{else if eq .Type "warning"}}
+
+ Warning {{.Message}}
+
+ {{else if eq .Type "error"}}
+
+ Error {{.Message}}
+
+ {{else if eq .Type "info"}}
+
+ Info {{.Message}}
+
+ {{end}}
+ {{end}}
+
+
+
+
+
+
add new host
+
+
+
+
add update ip Address
+
+
+
+
filter available hosts
+
+
+
+
+
+
+
+
+ | Hostname |
+ Token |
+ IP Addresses |
+ Actions |
+
+
+
+ {{range $key, $val := .Hosts }}
+
+ | {{$key}} |
+ {{$val}} |
+
+
+ {{range index $.IpAddresses $key}}
+ {{.}}
+ {{end}}
+ |
+
+ delete
+ resolve
+ external resolve (google)
+ |
+
+ {{end}}
+
+
+
+{{template "footer"}}
\ No newline at end of file
diff --git a/resources/templates/footer.html b/resources/templates/footer.html
new file mode 100644
index 0000000..37bdaad
--- /dev/null
+++ b/resources/templates/footer.html
@@ -0,0 +1,4 @@
+{{define "footer"}}
+