commit 7f2e6734ce0abf6d8fef8013620df474df5175e7 Author: Ambrose Chua Date: Sat Feb 16 20:35:47 2019 +0800 Initial commit diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..c95f942 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,15 @@ +kind: pipeline +name: default + +steps: +- name: docker + image: plugins/docker + settings: + registry: registry.makerforce.io + repo: registry.makerforce.io/cacti/he-dns-editor + tags: + - latest + username: + from_secret: docker_username + password: + from_secret: docker_password diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae064b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +he-dns-editor diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eaaf46d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM golang:1.11-alpine as build + +# args +ARG version="0.1" +ARG repo="git.makerforce.io/cacti/he-dns-editor" + +# dependencies +RUN apk add --no-cache ca-certificates + +# source +WORKDIR $GOPATH/src/${repo} +COPY . . + +# build +ENV CGO_ENABLED=0 +ENV GOOS=linux +ENV GOARCH=amd64 +RUN go build -ldflags "-s -w" -o /he-dns-editor + + +FROM scratch + +ARG version + +# labels +LABEL org.label-schema.vcs-url="https://git.makerforce.io/cacti/he-dns-editor" +LABEL org.label-schema.version=${version} +LABEL org.label-schema.schema-version="1.0" + +# copy binary and ca certs +COPY --from=build /he-dns-editor /email-collector +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +EXPOSE 8080 + +ENTRYPOINT ["/he-dns-editor"] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8ebb507 --- /dev/null +++ b/go.mod @@ -0,0 +1,6 @@ +module git.makerforce.io/cacti/he-dns-editor + +require ( + github.com/julienschmidt/httprouter v1.2.0 + golang.org/x/net v0.0.0-20190213061140-3a22650c66bd +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..61792db --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..9c894f7 --- /dev/null +++ b/main.go @@ -0,0 +1,203 @@ +package main // import "git.makerforce.io/cacti/he-dns-editor" + +import ( + "errors" + "flag" + "io" + "log" + "net/http" + "strings" + + "github.com/julienschmidt/httprouter" + "golang.org/x/net/html" +) + +var listen string +var username string +var password string +var zoneid string + +func init() { + flag.StringVar(&listen, "listen", ":8080", "Listen on port") + flag.StringVar(&username, "username", "", "HE.net username") + flag.StringVar(&password, "password", "", "HE.net password") + flag.StringVar(&zoneid, "zoneid", "", "HE.net zone ID") +} + +func main() { + flag.Parse() + if len(username) == 0 || len(password) == 0 || len(zoneid) == 0 { + flag.PrintDefaults() + log.Fatal("Please specify username, password and zoneid") + } + + router := httprouter.New() + router.GET("/", Index) + router.POST("/", Post) + + // Listen + log.Println("main: Listening on", listen) + log.Fatal(http.ListenAndServe(listen, router)) +} + +func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + w.Header().Add("Content-Type", "text/html") + records, err := request("", "", "", "", "") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("An error occurred")) + log.Println(err) + return + } + res := ` + +
` + for _, record := range records { + disabled := "" + if record.Type == "NS" || record.Type == "SOA" { + disabled = ` disabled` + } + res += `
` + res += `` + res += `` + res += `` + res += `` + res += `` + res += `` + res += `
` + } + res += `
` + res += `` + res += `` + res += `` + res += `` + res += `
` + res += `
` + w.Write([]byte(res)) +} + +func Post(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + _, err := request( + r.FormValue("editrecord"), + r.FormValue("recordid"), + r.FormValue("recordname"), + r.FormValue("recordtype"), + r.FormValue("recordcontent"), + ) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("An error occurred")) + log.Println(err) + return + } + w.Header().Add("Location", "/") + w.WriteHeader(http.StatusSeeOther) +} + +var BadResponseError = errors.New("Bad response") + +type Record struct { + ID string + Name string + Type string + Content string +} + +func request(editrecord, recordid, recordname, recordtype, recordcontent string) ([]Record, error) { + log.Println("Operation", editrecord, recordid, recordname, recordtype, recordcontent) + body := "email=" + username + "&pass=" + password + "&account=" + body += "&menu=edit_zone&hosted_dns_editzone=1&hosted_dns_zoneid=" + zoneid + if len(editrecord) > 0 { + if editrecord == "Delete" { + body += "&hosted_dns_delconfirm=DELETE&hosted_dns_delrecord=1" + } else { + body += "&hosted_dns_editrecord=" + editrecord + } + } + if len(recordid) > 0 { + body += "&hosted_dns_recordid=" + recordid + } + if len(recordname) > 0 { + body += "&Name=" + recordname + } + if len(recordtype) > 0 { + body += "&Type=" + recordtype + } + body += "&TTL=300" + if len(recordcontent) > 0 { + body += "&Content=" + recordcontent + } + + buf := strings.NewReader(body) + resp, err := http.Post("https://dns.he.net", "application/x-www-form-urlencoded", buf) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, BadResponseError + } + return parseRequest(resp) +} + +func parseRequest(resp *http.Response) ([]Record, error) { + d := html.NewTokenizer(resp.Body) + records := make([]Record, 0) + working := Record{} + tdCount := 0 + + var err error + for err == nil { + tok := d.Next() + switch tok { + case html.ErrorToken: + err = d.Err() + case html.TextToken: + text := string(d.Text()) + switch tdCount { + case 2: + working.Name = text + case 3: + working.Type = text + case 6: + working.Content = text + } + case html.StartTagToken: + tagName, _ := d.TagName() + tagAttr := make(map[string]string) + var k, v []byte + more := true + for more { + k, v, more = d.TagAttr() + tagAttr[string(k)] = string(v) + } + if string(tagName) == "tr" { + if tagAttr["class"] == "dns_tr" || tagAttr["class"] == "dns_tr_dynamic" { + tdCount = 0 + working = Record{} + } + if _, ok := tagAttr["id"]; ok { + working.ID = tagAttr["id"] + } + } + case html.EndTagToken: + tagName, _ := d.TagName() + if string(tagName) == "tr" { + if len(working.ID) == 0 { + break + } + records = append(records, working) + } else if string(tagName) == "td" { + tdCount += 1 + } + } + } + + if err == io.EOF { + err = nil + } + return records, err +}