Toby Chui 5f64b622b5 Fixed #353 and #327
- Added user defined polling and propagation timeout option in ACME
- Updated lego and added a few new DNS challenge providers
- Updated code gen to support new parameters
2024-10-27 16:17:44 +08:00

320 lines
8.4 KiB
Go

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")
}