package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"text/template"
"time"
)
const ICANN_GTLD_JSON_URL = "https://www.icann.org/resources/registries/gtlds/v2/gtlds.json"
var (
legacyGTLDs = map[string]bool{
"aero": true,
"asia": true,
"biz": true,
"cat": true,
"com": true,
"coop": true,
"info": true,
"jobs": true,
"mobi": true,
"museum": true,
"name": true,
"net": true,
"org": true,
"post": true,
"pro": true,
"tel": true,
"xxx": true,
}
pslTemplate = template.Must(template.New("public-suffix-list-gtlds").Parse(`
// List of new gTLDs imported from {{ .URL }} on {{ .Date.Format "2006-01-02T15:04:05Z07:00" }}
// This list is auto-generated, don't edit it manually.
{{- range .Entries }}
{{ .Comment }}
{{ printf "%s\n" .ULabel}}
{{- end }}
`))
)
type pslEntry struct {
ALabel string `json:"gTLD"`
ULabel string
RegistryOperator string
DateOfContractSignature string
ContractTerminated bool
RemovalDate string
}
func (e *pslEntry) normalize() {
e.ALabel = strings.TrimSpace(e.ALabel)
e.ULabel = strings.TrimSpace(e.ULabel)
e.RegistryOperator = strings.TrimSpace(e.RegistryOperator)
e.DateOfContractSignature = strings.TrimSpace(e.DateOfContractSignature)
if e.ULabel == "" {
e.ULabel = e.ALabel
}
}
func (e pslEntry) Comment() string {
parts := []string{
"//",
e.ALabel,
":",
e.DateOfContractSignature,
}
if e.RegistryOperator != "" {
parts = append(parts, e.RegistryOperator)
}
return strings.Join(parts, " ")
}
func getData(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code fetching data "+
"from %q : expected status %d got %d",
url, http.StatusOK, resp.StatusCode)
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return respBody, nil
}
func filterGTLDs(entries []*pslEntry) []*pslEntry {
var filtered []*pslEntry
for _, entry := range entries {
if _, isLegacy := legacyGTLDs[entry.ALabel]; isLegacy {
continue
}
if entry.ContractTerminated {
continue
}
if entry.RemovalDate != "" {
continue
}
filtered = append(filtered, entry)
}
return filtered
}
func getPSLEntries(url string) ([]*pslEntry, error) {
respBody, err := getData(url)
if err != nil {
return nil, err
}
var results struct {
GTLDs []*pslEntry
}
if err := json.Unmarshal(respBody, &results); err != nil {
return nil, fmt.Errorf(
"unmarshaling ICANN gTLD JSON data: %v", err)
}
if len(results.GTLDs) == 0 {
return nil, errors.New("found no gTLD information after unmarshaling")
}
for _, tldEntry := range results.GTLDs {
tldEntry.normalize()
}
filtered := filterGTLDs(results.GTLDs)
if len(filtered) == 0 {
return nil, errors.New(
"found no gTLD information after removing legacy and contract terminated gTLDs")
}
return filtered, nil
}
func renderData(entries []*pslEntry, writer io.Writer) error {
templateData := struct {
URL string
Date time.Time
Entries []*pslEntry
}{
URL: ICANN_GTLD_JSON_URL,
Date: time.Now(),
Entries: entries,
}
var buf bytes.Buffer
if err := pslTemplate.Execute(&buf, templateData); err != nil {
return err
}
_, err := writer.Write(buf.Bytes())
if err != nil {
return err
}
return nil
}
func main() {
ifErrQuit := func(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "error updating gTLD data: %v\n", err)
os.Exit(1)
}
}
entries, err := getPSLEntries(ICANN_GTLD_JSON_URL)
ifErrQuit(err)
err = renderData(entries, os.Stdout)
ifErrQuit(err)
}