package main import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "strings" ) /* Usage git clone {repo_link_for_lego} go run extract.go //go run extract.go -- "win7" */ var legoProvidersSourceFolder string = "./lego/providers/dns/" var outputDir string = "./acmedns" var defTemplate string = `package acmedns /* THIS MODULE IS GENERATED AUTOMATICALLY DO NOT EDIT THIS FILE */ import ( "encoding/json" "fmt" "time" "github.com/go-acme/lego/v4/challenge" {{imports}} ) //name is the DNS provider name, e.g. cloudflare or gandi //JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"} func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64, pollingInterval int64)(challenge.Provider, error){ pgDuration := time.Duration(propagationTimeout) * time.Second plInterval := time.Duration(pollingInterval) * time.Second switch name { {{magic}} default: return nil, fmt.Errorf("unrecognized DNS provider: %s", name) } } ` type Field struct { Title string Datatype string } type ProviderInfo struct { Name string //Name of this provider ConfigableFields []*Field //Field that shd be expose to user for filling in HiddenFields []*Field //Fields that is usable but shd be hidden from user } func fileExists(filePath string) bool { _, err := os.Stat(filePath) if err == nil { return true } if os.IsNotExist(err) { return false } // For other errors, you may handle them differently return false } // This function define the DNS not supported by zoraxy func getExcludedDNSProviders() []string { return []string{ "edgedns", //Too complex data structure "exec", //Not a DNS provider "httpreq", //Not a DNS provider //"hurricane", //Multi-credentials arch "oraclecloud", //Evil company "acmedns", //Not a DNS provider "selectelv2", //Not sure why not working with our code generator "designate", //OpenStack, if you are using this you shd not be using zoraxy "mythicbeasts", //Module require url.URL, which cannot be automatically parsed //The following are incomaptible with Zoraxy due to dependencies issue, //might be resolved in future "corenetworks", "timewebcloud", "volcengine", "exoscale", } } // Exclude list for Windows build, due to limitations for lego versions func getExcludedDNSProvidersNT61() []string { fmt.Println("Windows 7 support is deprecated, please consider upgrading to a newer version of Windows.") return append(getExcludedDNSProviders(), []string{"cpanel", "mailinabox", "shellrent", }...) } func isInSlice(str string, slice []string) bool { for _, s := range slice { if s == str { return true } } return false } func isExcludedDNSProvider(providerName string) bool { return isInSlice(providerName, getExcludedDNSProviders()) } func isExcludedDNSProviderNT61(providerName string) bool { return isInSlice(providerName, getExcludedDNSProvidersNT61()) } // extractConfigStruct extracts the name of the config struct and its content as plain text from a given source code. func extractConfigStruct(sourceCode string) (string, string) { // Regular expression to match the struct declaration. structRegex := regexp.MustCompile(`type\s+([A-Za-z0-9_]+)\s+struct\s*{([^{}]*)}`) // Find the first match of the struct declaration. match := structRegex.FindStringSubmatch(sourceCode) if len(match) < 3 { return "", "" // No match found } // Extract the struct name and its content. structName := match[1] structContent := match[2] if structName != "Config" { allStructs := structRegex.FindAllStringSubmatch(sourceCode, 10) for _, thisStruct := range allStructs { //fmt.Println("Name => ", test[1]) //fmt.Println("Content => ", test[2]) if thisStruct[1] == "Config" { structName = "Config" structContent = thisStruct[2] break } } if structName != "Config" { panic("Unable to find Config for this provider") } } return structName, structContent } func main() { // A map of provider name to information on what can be filled extractedProviderList := map[string]*ProviderInfo{} buildForWindowsSeven := (len(os.Args) > 2 && os.Args[2] == "win7") //Search all providers providers, err := filepath.Glob(filepath.Join(legoProvidersSourceFolder, "/*")) if err != nil { panic(err) } //Create output folder if not exists err = os.MkdirAll(outputDir, 0775) if err != nil { panic(err) } generatedConvertcode := "" importList := "" for _, provider := range providers { providerName := filepath.Base(provider) if buildForWindowsSeven { if isExcludedDNSProviderNT61(providerName) { //Ignore this provider continue } } else { if isExcludedDNSProvider(providerName) { //Ignore this provider continue } } //Check if {provider_name}.go exists providerDef := filepath.Join(provider, providerName+".go") if !fileExists(providerDef) { continue } fmt.Println("Extracting config structure for: " + providerDef) providerSrc, err := os.ReadFile(providerDef) if err != nil { fmt.Println(err.Error()) return } _, strctContent := extractConfigStruct(string(providerSrc)) //Filter and write the content to json /* Example of stctContent (Note the tab prefix) Token string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration HTTPClient *http.Client */ strctContentLines := strings.Split(strctContent, "\n") configKeys := []*Field{} hiddenKeys := []*Field{} for _, lineDef := range strctContentLines { fields := strings.Fields(lineDef) if len(fields) < 2 || strings.HasPrefix(fields[0], "//") { //Ignore this line continue } //Filter out the fields that is not user-filled switch fields[1] { case "*url.URL": fallthrough case "string": //Add exception rule for gandi baseURL if (providerName == "gandi" || providerName == "gandiv5") && fields[0] == "BaseURL" { //Not useful stuff. Ignore this field continue } configKeys = append(configKeys, &Field{ Title: fields[0], Datatype: fields[1], }) case "int": if fields[0] != "TTL" { configKeys = append(configKeys, &Field{ Title: fields[0], Datatype: fields[1], }) } else if fields[0] == "TTL" { //haveTTLField = true } else { hiddenKeys = append(hiddenKeys, &Field{ Title: fields[0], Datatype: fields[1], }) } case "bool": if fields[0] == "InsecureSkipVerify" || fields[0] == "SSLVerify" || fields[0] == "Debug" { configKeys = append(configKeys, &Field{ Title: fields[0], Datatype: fields[1], }) } else { hiddenKeys = append(hiddenKeys, &Field{ Title: fields[0], Datatype: fields[1], }) } case "time.Duration": if fields[0] == "PropagationTimeout" || fields[0] == "PollingInterval" { configKeys = append(configKeys, &Field{ Title: fields[0], Datatype: fields[1], }) } default: //Not used fields hiddenKeys = append(hiddenKeys, &Field{ Title: fields[0], Datatype: fields[1], }) } } fmt.Println(strctContent) extractedProviderList[providerName] = &ProviderInfo{ Name: providerName, ConfigableFields: configKeys, HiddenFields: hiddenKeys, } //Generate the code for converting incoming json into target config codeSegment := ` case "` + providerName + `": cfg := ` + providerName + `.NewDefaultConfig() err := json.Unmarshal([]byte(js), &cfg) if err != nil { return nil, err } cfg.PropagationTimeout = pgDuration cfg.PollingInterval = plInterval return ` + providerName + `.NewDNSProviderConfig(cfg)` generatedConvertcode += codeSegment importList += ` "github.com/go-acme/lego/v4/providers/dns/` + providerName + "\"\n" } js, err := json.MarshalIndent(extractedProviderList, "", " ") if err != nil { panic(err) } fullCodeSnippet := strings.ReplaceAll(defTemplate, "{{magic}}", generatedConvertcode) fullCodeSnippet = strings.ReplaceAll(fullCodeSnippet, "{{imports}}", importList) outJsonFilename := "providers.json" outGoFilename := "acmedns.go" if buildForWindowsSeven { outJsonFilename = "providers_nt61.json" outGoFilename = "acmedns_nt61.go" } os.WriteFile(filepath.Join(outputDir, outJsonFilename), js, 0775) os.WriteFile(filepath.Join(outputDir, outGoFilename), []byte(fullCodeSnippet), 0775) fmt.Println("Output written to file") }