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}}
+
+
+ Logout +
+

{{.Profile}}

+
+
+ +
+
+
+ +
+ +
+ {{range .Alerts}} + {{if eq .Type "success"}} + + {{else if eq .Type "warning"}} + + {{else if eq .Type "error"}} + + {{else if eq .Type "info"}} + + {{end}} + {{end}} +
+ +
+
+
+
add new host
+
+ + + + +
+
+
+
add update ip Address
+
+ + + + + + +
+
+
+
filter available hosts
+
+ + + + +
+
+
+
+ + + + + + + + + + + + {{range $key, $val := .Hosts }} + + + + + + + + {{end}} + +
HostnameTokenIP AddressesActions
{{$key}}{{$val}} + {{range index $.IpAddresses $key}} + {{.}}
+ {{end}} +
+ delete + resolve + external resolve (google) +
+
+{{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"}} + + +{{end}} \ No newline at end of file diff --git a/resources/templates/header.html b/resources/templates/header.html new file mode 100644 index 0000000..a729d40 --- /dev/null +++ b/resources/templates/header.html @@ -0,0 +1,32 @@ +{{define "header"}} + + + + + + + + + + ddnsService + + + + + + + + + + + +{{end}} \ No newline at end of file