package main import ( "crypto/ecdsa" "crypto/rsa" "encoding/json" "fmt" "io" "net/http" "net/mail" "net/url" "os" "path/filepath" "reflect" "regexp" "strings" "time" "github.com/spf13/cobra" "github.com/authelia/authelia/v4/internal/configuration/schema" ) func newCodeCmd() *cobra.Command { cmd := &cobra.Command{ Use: cmdUseCode, Short: "Generate code", RunE: rootSubCommandsRunE, DisableAutoGenTag: true, } cmd.AddCommand(newCodeKeysCmd(), newCodeServerCmd(), newCodeScriptsCmd()) return cmd } func newCodeServerCmd() *cobra.Command { cmd := &cobra.Command{ Use: cmdUseServer, Short: "Generate the Authelia server files", RunE: codeServerRunE, DisableAutoGenTag: true, } return cmd } func newCodeScriptsCmd() *cobra.Command { cmd := &cobra.Command{ Use: cmdUseCodeScripts, Short: "Generate the generated portion of the authelia-scripts command", RunE: codeScriptsRunE, DisableAutoGenTag: true, } return cmd } func newCodeKeysCmd() *cobra.Command { cmd := &cobra.Command{ Use: cmdUseKeys, Short: "Generate the list of valid configuration keys", RunE: codeKeysRunE, DisableAutoGenTag: true, } return cmd } func codeServerRunE(cmd *cobra.Command, args []string) (err error) { data := TemplateCSP{ PlaceholderNONCE: codeCSPNonce, TemplateDefault: buildCSP(codeCSPProductionDefaultSrc, codeCSPValuesCommon, codeCSPValuesProduction), TemplateDevelopment: buildCSP(codeCSPDevelopmentDefaultSrc, codeCSPValuesCommon, codeCSPValuesDevelopment), } var outputPath string if outputPath, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagFileServerGenerated); err != nil { return err } var f *os.File if f, err = os.Create(outputPath); err != nil { return fmt.Errorf("failed to create file '%s': %w", outputPath, err) } if err = tmplServer.Execute(f, data); err != nil { _ = f.Close() return fmt.Errorf("failed to write output file '%s': %w", outputPath, err) } if err = f.Close(); err != nil { return fmt.Errorf("failed to close output file '%s': %w", outputPath, err) } return nil } func codeScriptsRunE(cmd *cobra.Command, args []string) (err error) { var ( root, pathScriptsGen string resp *http.Response ) data := &tmplScriptsGEnData{} if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { return err } if pathScriptsGen, err = cmd.Flags().GetString(cmdFlagFileScriptsGen); err != nil { return err } if data.Package, err = cmd.Flags().GetString(cmdFlagPackageScriptsGen); err != nil { return err } if resp, err = http.Get("https://api.github.com/repos/swagger-api/swagger-ui/tags"); err != nil { return fmt.Errorf("failed to get latest version of the Swagger UI: %w", err) } defer resp.Body.Close() var ( respJSON []GitHubTagsJSON respRaw []byte ) if respRaw, err = io.ReadAll(resp.Body); err != nil { return fmt.Errorf("failed to get latest version of the Swagger UI: %w", err) } if err = json.Unmarshal(respRaw, &respJSON); err != nil { return fmt.Errorf("failed to get latest version of the Swagger UI: %w", err) } if len(respJSON) < 1 { return fmt.Errorf("failed to get latest version of the Swagger UI: the api returned zero results") } if strings.HasPrefix(respJSON[0].Name, "v") { data.VersionSwaggerUI = respJSON[0].Name[1:] } else { data.VersionSwaggerUI = respJSON[0].Name } fullPathScriptsGen := filepath.Join(root, pathScriptsGen) var f *os.File if f, err = os.Create(fullPathScriptsGen); err != nil { return fmt.Errorf("failed to create file '%s': %w", fullPathScriptsGen, err) } if err = tmplScriptsGen.Execute(f, data); err != nil { _ = f.Close() return fmt.Errorf("failed to write output file '%s': %w", fullPathScriptsGen, err) } if err = f.Close(); err != nil { return fmt.Errorf("failed to close output file '%s': %w", fullPathScriptsGen, err) } return nil } func codeKeysRunE(cmd *cobra.Command, args []string) (err error) { var ( pathCodeConfigKeys, root string f *os.File ) data := tmplConfigurationKeysData{ Timestamp: time.Now(), Keys: readTags("", reflect.TypeOf(schema.Configuration{})), } if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { return err } if pathCodeConfigKeys, err = cmd.Flags().GetString(cmdFlagFileConfigKeys); err != nil { return err } if data.Package, err = cmd.Flags().GetString(cmdFlagPackageConfigKeys); err != nil { return err } fullPathCodeConfigKeys := filepath.Join(root, pathCodeConfigKeys) if f, err = os.Create(fullPathCodeConfigKeys); err != nil { return fmt.Errorf("failed to create file '%s': %w", fullPathCodeConfigKeys, err) } if err = tmplCodeConfigurationSchemaKeys.Execute(f, data); err != nil { _ = f.Close() return fmt.Errorf("failed to write output file '%s': %w", fullPathCodeConfigKeys, err) } if err = f.Close(); err != nil { return fmt.Errorf("failed to close output file '%s': %w", fullPathCodeConfigKeys, err) } return nil } var decodedTypes = []reflect.Type{ reflect.TypeOf(mail.Address{}), reflect.TypeOf(regexp.Regexp{}), reflect.TypeOf(url.URL{}), reflect.TypeOf(time.Duration(0)), reflect.TypeOf(schema.Address{}), reflect.TypeOf(rsa.PrivateKey{}), reflect.TypeOf(ecdsa.PrivateKey{}), } func containsType(needle reflect.Type, haystack []reflect.Type) (contains bool) { for _, t := range haystack { if needle.Kind() == reflect.Ptr { if needle.Elem() == t { return true } } else if needle == t { return true } } return false } //nolint:gocyclo func readTags(prefix string, t reflect.Type) (tags []string) { tags = make([]string, 0) if t.Kind() != reflect.Struct { if t.Kind() == reflect.Slice { tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, "", true), t.Elem())...) } return } for i := 0; i < t.NumField(); i++ { field := t.Field(i) tag := field.Tag.Get("koanf") if tag == "" { tags = append(tags, prefix) continue } switch field.Type.Kind() { case reflect.Struct: if !containsType(field.Type, decodedTypes) { tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type)...) continue } case reflect.Slice: switch field.Type.Elem().Kind() { case reflect.Struct: if !containsType(field.Type.Elem(), decodedTypes) { tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) continue } case reflect.Slice: tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) } case reflect.Ptr: switch field.Type.Elem().Kind() { case reflect.Struct: if !containsType(field.Type.Elem(), decodedTypes) { tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type.Elem())...) continue } case reflect.Slice: if field.Type.Elem().Elem().Kind() == reflect.Struct { if !containsType(field.Type.Elem(), decodedTypes) { tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) continue } } } } tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) } return tags } func getKeyNameFromTagAndPrefix(prefix, name string, slice bool) string { nameParts := strings.SplitN(name, ",", 2) if prefix == "" { return nameParts[0] } if len(nameParts) == 2 && nameParts[1] == "squash" { return prefix } if slice { if name == "" { return fmt.Sprintf("%s[]", prefix) } return fmt.Sprintf("%s.%s[]", prefix, nameParts[0]) } return fmt.Sprintf("%s.%s", prefix, nameParts[0]) }