--- /dev/null
+package main
+
+import (
+ "net"
+ "net/http"
+ "log"
+ "strings"
+ "os"
+ "os/exec"
+ "io"
+ "io/ioutil"
+ "bufio"
+ "encoding/base64"
+ "crypto/md5"
+ "fmt"
+)
+
+//TODO add parsing of cmdline
+const (
+ AccFile = "/usr/local/etc/ddnset/ddnset.acc"
+ ZoneFile = "/usr/local/etc/ddnset/ddnset.zon"
+ Logfile = "/var/log/ddnset/default.log"
+ ListenPort = "1980"
+)
+
+const (
+ StatusGood = "good"
+ StatusBadAuth = "badauth"
+ StatusBadDay = "911"
+ StatusNotFQHN = "notfqdn"
+ StatusBadHost = "nohost"
+ StatusBadZone = "dnserr" //???
+ StatusBadIP = "badip"
+)
+
+
+func isPrivate (ip net.IP) bool {
+ if ip4 := ip.To4(); ip4 != nil {
+ return ( (10 == ip4[0] ) || ( 192 == ip4[0] && 168 == ip4[1] ) || ( 172 == ip4[0] && (ip4[1] >= 16 && ip4[1] <= 31) ))
+ }
+ //TODO what about them new v6 addresses!?
+ return false;
+}
+
+// isDomainName checks if a string is a presentation-format domain name
+// (currently restricted to hostname-compatible "preferred name" LDH labels and
+// SRV-like "underscore labels"; see golang.org/issue/12421).
+func isDomainName(s string) bool {
+ // See RFC 1035, RFC 3696.
+ // Presentation format has dots before every label except the first, and the
+ // terminal empty label is optional here because we assume fully-qualified
+ // (absolute) input. We must therefore reserve space for the first and last
+ // labels' length octets in wire format, where they are necessary and the
+ // maximum total length is 255.
+ // So our _effective_ maximum is 253, but 254 is not rejected if the last
+ // character is a dot.
+ l := len(s)
+ if l == 0 || l > 254 || l == 254 && s[l-1] != '.' {
+ return false
+ }
+
+ last := byte('.')
+ ok := false // Ok once we've seen a letter.
+ partlen := 0
+ for i := 0; i < len(s); i++ {
+ c := s[i]
+ switch {
+ default:
+ return false
+ case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_':
+ ok = true
+ partlen++
+ case '0' <= c && c <= '9':
+ // fine
+ partlen++
+ case c == '-':
+ // Byte before dash cannot be dot.
+ if last == '.' {
+ return false
+ }
+ partlen++
+ case c == '.':
+ // Byte before dot cannot be dot, dash.
+ if last == '.' || last == '-' {
+ return false
+ }
+ if partlen > 63 || partlen == 0 {
+ return false
+ }
+ partlen = 0
+ }
+ last = c
+ }
+ if last == '-' || partlen > 63 {
+ return false
+ }
+
+ return ok
+}
+
+func checkIP (s string) (string, int, bool) {
+ valid := true
+ ipver := 4
+ //parse the hell out of it
+ ip := net.ParseIP (s);
+ if nil == ip.To4() { ipver = 6 }
+ if isPrivate(ip) { valid = false }
+ if !ip.IsGlobalUnicast() { valid = false }
+ return ip.String(), ipver, valid;
+}
+
+func isValidAuth (usr, psw string) (bool, string) {
+ pwentry := fmt.Sprintf("%s:%x", usr, md5.Sum([]byte(psw)))
+ acc,err := os.Open(AccFile)
+ if (nil == err){
+ defer acc.Close()
+ scanner := bufio.NewScanner(acc)
+ for scanner.Scan(){
+ line := scanner.Text()
+ if (strings.HasPrefix(line, pwentry)) {
+ //log.Print ("Irasshai!")
+ return true, strings.TrimPrefix (line, pwentry + ":")
+ }
+ }
+ }
+ //log.Print ("Okotowari yo!")
+ return false, ""
+}
+
+func isValidZone (h string) (bool, string) {
+ zon,err := os.Open(ZoneFile)
+ if (nil == err){
+ defer zon.Close()
+ scanner := bufio.NewScanner(zon)
+ for scanner.Scan(){
+ line := scanner.Text()
+ if (strings.HasSuffix(h, line)) {
+// log.Printf ("%s is in zone %s", h, line)
+ return true, line
+ }
+ }
+ }
+// log.Printf ("No zone for the wicked %s", h)
+ return false, ""
+}
+
+//TODO implement no temp file version
+func knsupdate(host, ip, zone string, ver int) bool {
+ rectype := "A"
+ if (6 == ver) { rectype = "AAAA" }
+ tmpfile, err := ioutil.TempFile("", "knsupdate")
+ if err != nil {
+ log.Print(err)
+ return false
+ }
+ //log.Print ("Using ", tmpfile.Name())
+ defer os.Remove(tmpfile.Name()) // clean up
+ fmt.Fprintf (tmpfile, "server 127.0.0.1\n");
+ fmt.Fprintf (tmpfile, "zone %s\n", zone);
+ fmt.Fprintf (tmpfile, "del %s 300 IN %s\n", host, rectype);
+ fmt.Fprintf (tmpfile, "add %s 300 IN %s %s\n", host, rectype, ip);
+ fmt.Fprintf (tmpfile, "send\n");
+ tmpfile.Close()
+ out, err := exec.Command("/usr/bin/knsupdate", tmpfile.Name()).Output()
+
+ if err != nil {
+ log.Print(err)
+ return false
+ }
+
+ if (strings.Contains(string(out), "Error")) {
+ log.Printf("%q", out);
+ return false;
+ }
+
+ return true
+}
+
+
+
+func myipHandle(w http.ResponseWriter, r *http.Request){
+ log.Print(r.RemoteAddr, " ", r.Method, " ", r.URL.String());
+ clientIP, _, err := net.SplitHostPort(r.RemoteAddr);
+ //TODO support more proxy headers and
+ //walk them until first valid address
+ //fallback to r.RemoteAddr if none are available
+ if nil != err { log.Print(err); return ;}
+ if prior, ok := r.Header["X-Forwarded-For"]; ok {
+ log.Print ("XFF header present")
+ clientIP = strings.Split(prior[0], ",")[0]
+ }
+ clientIP, _, _ = checkIP(clientIP)
+ log.Printf ("Client IP = %s", clientIP);
+ io.WriteString(w, clientIP)
+}
+
+func rootHandle(w http.ResponseWriter, r *http.Request){
+
+ log.Print(r.RemoteAddr, " ", r.Method, " ", r.URL.String());
+ //check auth
+ authok := false
+ hosts := ""
+ if hdr_auth, ok := r.Header["Authorization"]; ok {
+ log.Print ("Authorization header present")
+ for _, auth := range (hdr_auth) {
+ auth := strings.Split (auth, " ")
+ log.Print ("Method: ", auth[0])
+ if ("Basic" == auth[0]){
+ authdec, err := base64.StdEncoding.DecodeString(auth[1])
+ if nil != err { continue }
+ usrpw := strings.Split (string(authdec), ":")
+ log.Printf ("User: %s", usrpw[0]);
+ authok, hosts = isValidAuth(usrpw[0], usrpw[1])
+ }
+ }
+ }
+ if false == authok {
+ log.Print("StatusBadAuth");
+ io.WriteString(w, StatusBadAuth + "\n");
+ return;
+ }
+
+ log.Print ("User hosts: ", hosts);
+ //check ip
+ q := r.URL.Query()
+ hostnames := q["hostname"]
+ myip, ver, valid := checkIP(q["myip"][0]) //one IP to rule them all
+ log.Printf("IP: %s (valid:%v, ver:%d)", myip, valid, ver);
+ if (!valid){
+ log.Print("StatusBadIP");
+ io.WriteString(w, StatusBadIP + "\n");
+ return;
+ }
+ //update
+ for _, host := range (hostnames) {
+ if !isDomainName(host) {
+ log.Print("StatusNotFQHN");
+ io.WriteString(w, StatusNotFQHN + "\n");
+ continue
+ }
+ vz, zone := isValidZone(host)
+ if !vz {
+ log.Print("StatusBadZone");
+ io.WriteString(w, StatusBadZone + "\n");
+ continue
+ }
+ if (strings.Contains (hosts, host)) {
+ log.Printf ("Updating %s to IP %s", host, myip);
+ updated := knsupdate (host, myip, zone, ver)
+ if updated {
+ log.Print("StatusGood");
+ io.WriteString(w, StatusGood + "\n");
+ } else {
+ log.Print("StatusBadDay");
+ io.WriteString(w, StatusBadDay + "\n");
+ }
+ } else {
+ log.Print("StatusBadHost");
+ io.WriteString(w, StatusBadHost + "\n");
+ }
+ }
+}
+
+func main() {
+ http.HandleFunc("/myip", myipHandle);
+ http.HandleFunc("/myip.php", myipHandle);
+ http.HandleFunc("/", rootHandle);
+ logf, err := os.OpenFile(Logfile, os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0644)
+ if nil == err{
+ defer logf.Close()
+ log.SetOutput (logf)
+ } else {
+ log.Print (err)
+ }
+
+ log.Fatal(http.ListenAndServe(":" + ListenPort, nil));
+}