github.com/jcmturner/gokrb5/v8@v8.4.4/config/krb5conf.go (about)

     1  // Package config implements KRB5 client and service configuration as described at https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html
     2  package config
     3  
     4  import (
     5  	"bufio"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"os"
    13  	"os/user"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/jcmturner/gofork/encoding/asn1"
    20  	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
    21  )
    22  
    23  // Config represents the KRB5 configuration.
    24  type Config struct {
    25  	LibDefaults LibDefaults
    26  	Realms      []Realm
    27  	DomainRealm DomainRealm
    28  	//CaPaths
    29  	//AppDefaults
    30  	//Plugins
    31  }
    32  
    33  // WeakETypeList is a list of encryption types that have been deemed weak.
    34  const WeakETypeList = "des-cbc-crc des-cbc-md4 des-cbc-md5 des-cbc-raw des3-cbc-raw des-hmac-sha1 arcfour-hmac-exp rc4-hmac-exp arcfour-hmac-md5-exp des"
    35  
    36  // New creates a new config struct instance.
    37  func New() *Config {
    38  	d := make(DomainRealm)
    39  	return &Config{
    40  		LibDefaults: newLibDefaults(),
    41  		DomainRealm: d,
    42  	}
    43  }
    44  
    45  // LibDefaults represents the [libdefaults] section of the configuration.
    46  type LibDefaults struct {
    47  	AllowWeakCrypto bool //default false
    48  	// ap_req_checksum_type int //unlikely to support this
    49  	Canonicalize bool          //default false
    50  	CCacheType   int           //default is 4. unlikely to implement older
    51  	Clockskew    time.Duration //max allowed skew in seconds, default 300
    52  	//Default_ccache_name string // default /tmp/krb5cc_%{uid} //Not implementing as will hold in memory
    53  	DefaultClientKeytabName string //default /usr/local/var/krb5/user/%{euid}/client.keytab
    54  	DefaultKeytabName       string //default /etc/krb5.keytab
    55  	DefaultRealm            string
    56  	DefaultTGSEnctypes      []string //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
    57  	DefaultTktEnctypes      []string //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
    58  	DefaultTGSEnctypeIDs    []int32  //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
    59  	DefaultTktEnctypeIDs    []int32  //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
    60  	DNSCanonicalizeHostname bool     //default true
    61  	DNSLookupKDC            bool     //default false
    62  	DNSLookupRealm          bool
    63  	ExtraAddresses          []net.IP       //Not implementing yet
    64  	Forwardable             bool           //default false
    65  	IgnoreAcceptorHostname  bool           //default false
    66  	K5LoginAuthoritative    bool           //default false
    67  	K5LoginDirectory        string         //default user's home directory. Must be owned by the user or root
    68  	KDCDefaultOptions       asn1.BitString //default 0x00000010 (KDC_OPT_RENEWABLE_OK)
    69  	KDCTimeSync             int            //default 1
    70  	//kdc_req_checksum_type int //unlikely to implement as for very old KDCs
    71  	NoAddresses         bool     //default true
    72  	PermittedEnctypes   []string //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
    73  	PermittedEnctypeIDs []int32
    74  	//plugin_base_dir string //not supporting plugins
    75  	PreferredPreauthTypes []int         //default “17, 16, 15, 14”, which forces libkrb5 to attempt to use PKINIT if it is supported
    76  	Proxiable             bool          //default false
    77  	RDNS                  bool          //default true
    78  	RealmTryDomains       int           //default -1
    79  	RenewLifetime         time.Duration //default 0
    80  	SafeChecksumType      int           //default 8
    81  	TicketLifetime        time.Duration //default 1 day
    82  	UDPPreferenceLimit    int           // 1 means to always use tcp. MIT krb5 has a default value of 1465, and it prevents user setting more than 32700.
    83  	VerifyAPReqNofail     bool          //default false
    84  }
    85  
    86  // Create a new LibDefaults struct.
    87  func newLibDefaults() LibDefaults {
    88  	uid := "0"
    89  	var hdir string
    90  	usr, _ := user.Current()
    91  	if usr != nil {
    92  		uid = usr.Uid
    93  		hdir = usr.HomeDir
    94  	}
    95  	opts := asn1.BitString{}
    96  	opts.Bytes, _ = hex.DecodeString("00000010")
    97  	opts.BitLength = len(opts.Bytes) * 8
    98  	l := LibDefaults{
    99  		CCacheType:              4,
   100  		Clockskew:               time.Duration(300) * time.Second,
   101  		DefaultClientKeytabName: fmt.Sprintf("/usr/local/var/krb5/user/%s/client.keytab", uid),
   102  		DefaultKeytabName:       "/etc/krb5.keytab",
   103  		DefaultTGSEnctypes:      []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "des3-cbc-sha1", "arcfour-hmac-md5", "camellia256-cts-cmac", "camellia128-cts-cmac", "des-cbc-crc", "des-cbc-md5", "des-cbc-md4"},
   104  		DefaultTktEnctypes:      []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "des3-cbc-sha1", "arcfour-hmac-md5", "camellia256-cts-cmac", "camellia128-cts-cmac", "des-cbc-crc", "des-cbc-md5", "des-cbc-md4"},
   105  		DNSCanonicalizeHostname: true,
   106  		K5LoginDirectory:        hdir,
   107  		KDCDefaultOptions:       opts,
   108  		KDCTimeSync:             1,
   109  		NoAddresses:             true,
   110  		PermittedEnctypes:       []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "des3-cbc-sha1", "arcfour-hmac-md5", "camellia256-cts-cmac", "camellia128-cts-cmac", "des-cbc-crc", "des-cbc-md5", "des-cbc-md4"},
   111  		RDNS:                    true,
   112  		RealmTryDomains:         -1,
   113  		SafeChecksumType:        8,
   114  		TicketLifetime:          time.Duration(24) * time.Hour,
   115  		UDPPreferenceLimit:      1465,
   116  		PreferredPreauthTypes:   []int{17, 16, 15, 14},
   117  	}
   118  	l.DefaultTGSEnctypeIDs = parseETypes(l.DefaultTGSEnctypes, l.AllowWeakCrypto)
   119  	l.DefaultTktEnctypeIDs = parseETypes(l.DefaultTktEnctypes, l.AllowWeakCrypto)
   120  	l.PermittedEnctypeIDs = parseETypes(l.PermittedEnctypes, l.AllowWeakCrypto)
   121  	return l
   122  }
   123  
   124  // Parse the lines of the [libdefaults] section of the configuration into the LibDefaults struct.
   125  func (l *LibDefaults) parseLines(lines []string) error {
   126  	for _, line := range lines {
   127  		//Remove comments after the values
   128  		if idx := strings.IndexAny(line, "#;"); idx != -1 {
   129  			line = line[:idx]
   130  		}
   131  		line = strings.TrimSpace(line)
   132  		if line == "" {
   133  			continue
   134  		}
   135  		if !strings.Contains(line, "=") {
   136  			return InvalidErrorf("libdefaults section line (%s)", line)
   137  		}
   138  
   139  		p := strings.Split(line, "=")
   140  		key := strings.TrimSpace(strings.ToLower(p[0]))
   141  		switch key {
   142  		case "allow_weak_crypto":
   143  			v, err := parseBoolean(p[1])
   144  			if err != nil {
   145  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   146  			}
   147  			l.AllowWeakCrypto = v
   148  		case "canonicalize":
   149  			v, err := parseBoolean(p[1])
   150  			if err != nil {
   151  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   152  			}
   153  			l.Canonicalize = v
   154  		case "ccache_type":
   155  			p[1] = strings.TrimSpace(p[1])
   156  			v, err := strconv.ParseUint(p[1], 10, 32)
   157  			if err != nil || v < 0 || v > 4 {
   158  				return InvalidErrorf("libdefaults section line (%s)", line)
   159  			}
   160  			l.CCacheType = int(v)
   161  		case "clockskew":
   162  			d, err := parseDuration(p[1])
   163  			if err != nil {
   164  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   165  			}
   166  			l.Clockskew = d
   167  		case "default_client_keytab_name":
   168  			l.DefaultClientKeytabName = strings.TrimSpace(p[1])
   169  		case "default_keytab_name":
   170  			l.DefaultKeytabName = strings.TrimSpace(p[1])
   171  		case "default_realm":
   172  			l.DefaultRealm = strings.TrimSpace(p[1])
   173  		case "default_tgs_enctypes":
   174  			l.DefaultTGSEnctypes = strings.Fields(p[1])
   175  		case "default_tkt_enctypes":
   176  			l.DefaultTktEnctypes = strings.Fields(p[1])
   177  		case "dns_canonicalize_hostname":
   178  			v, err := parseBoolean(p[1])
   179  			if err != nil {
   180  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   181  			}
   182  			l.DNSCanonicalizeHostname = v
   183  		case "dns_lookup_kdc":
   184  			v, err := parseBoolean(p[1])
   185  			if err != nil {
   186  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   187  			}
   188  			l.DNSLookupKDC = v
   189  		case "dns_lookup_realm":
   190  			v, err := parseBoolean(p[1])
   191  			if err != nil {
   192  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   193  			}
   194  			l.DNSLookupRealm = v
   195  		case "extra_addresses":
   196  			ipStr := strings.TrimSpace(p[1])
   197  			for _, ip := range strings.Split(ipStr, ",") {
   198  				if eip := net.ParseIP(ip); eip != nil {
   199  					l.ExtraAddresses = append(l.ExtraAddresses, eip)
   200  				}
   201  			}
   202  		case "forwardable":
   203  			v, err := parseBoolean(p[1])
   204  			if err != nil {
   205  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   206  			}
   207  			l.Forwardable = v
   208  		case "ignore_acceptor_hostname":
   209  			v, err := parseBoolean(p[1])
   210  			if err != nil {
   211  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   212  			}
   213  			l.IgnoreAcceptorHostname = v
   214  		case "k5login_authoritative":
   215  			v, err := parseBoolean(p[1])
   216  			if err != nil {
   217  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   218  			}
   219  			l.K5LoginAuthoritative = v
   220  		case "k5login_directory":
   221  			l.K5LoginDirectory = strings.TrimSpace(p[1])
   222  		case "kdc_default_options":
   223  			v := strings.TrimSpace(p[1])
   224  			v = strings.Replace(v, "0x", "", -1)
   225  			b, err := hex.DecodeString(v)
   226  			if err != nil {
   227  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   228  			}
   229  			l.KDCDefaultOptions.Bytes = b
   230  			l.KDCDefaultOptions.BitLength = len(b) * 8
   231  		case "kdc_timesync":
   232  			p[1] = strings.TrimSpace(p[1])
   233  			v, err := strconv.ParseInt(p[1], 10, 32)
   234  			if err != nil || v < 0 {
   235  				return InvalidErrorf("libdefaults section line (%s)", line)
   236  			}
   237  			l.KDCTimeSync = int(v)
   238  		case "noaddresses":
   239  			v, err := parseBoolean(p[1])
   240  			if err != nil {
   241  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   242  			}
   243  			l.NoAddresses = v
   244  		case "permitted_enctypes":
   245  			l.PermittedEnctypes = strings.Fields(p[1])
   246  		case "preferred_preauth_types":
   247  			p[1] = strings.TrimSpace(p[1])
   248  			t := strings.Split(p[1], ",")
   249  			var v []int
   250  			for _, s := range t {
   251  				i, err := strconv.ParseInt(s, 10, 32)
   252  				if err != nil {
   253  					return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   254  				}
   255  				v = append(v, int(i))
   256  			}
   257  			l.PreferredPreauthTypes = v
   258  		case "proxiable":
   259  			v, err := parseBoolean(p[1])
   260  			if err != nil {
   261  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   262  			}
   263  			l.Proxiable = v
   264  		case "rdns":
   265  			v, err := parseBoolean(p[1])
   266  			if err != nil {
   267  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   268  			}
   269  			l.RDNS = v
   270  		case "realm_try_domains":
   271  			p[1] = strings.TrimSpace(p[1])
   272  			v, err := strconv.ParseInt(p[1], 10, 32)
   273  			if err != nil || v < -1 {
   274  				return InvalidErrorf("libdefaults section line (%s)", line)
   275  			}
   276  			l.RealmTryDomains = int(v)
   277  		case "renew_lifetime":
   278  			d, err := parseDuration(p[1])
   279  			if err != nil {
   280  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   281  			}
   282  			l.RenewLifetime = d
   283  		case "safe_checksum_type":
   284  			p[1] = strings.TrimSpace(p[1])
   285  			v, err := strconv.ParseInt(p[1], 10, 32)
   286  			if err != nil || v < 0 {
   287  				return InvalidErrorf("libdefaults section line (%s)", line)
   288  			}
   289  			l.SafeChecksumType = int(v)
   290  		case "ticket_lifetime":
   291  			d, err := parseDuration(p[1])
   292  			if err != nil {
   293  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   294  			}
   295  			l.TicketLifetime = d
   296  		case "udp_preference_limit":
   297  			p[1] = strings.TrimSpace(p[1])
   298  			v, err := strconv.ParseUint(p[1], 10, 32)
   299  			if err != nil || v > 32700 {
   300  				return InvalidErrorf("libdefaults section line (%s)", line)
   301  			}
   302  			l.UDPPreferenceLimit = int(v)
   303  		case "verify_ap_req_nofail":
   304  			v, err := parseBoolean(p[1])
   305  			if err != nil {
   306  				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
   307  			}
   308  			l.VerifyAPReqNofail = v
   309  		}
   310  	}
   311  	l.DefaultTGSEnctypeIDs = parseETypes(l.DefaultTGSEnctypes, l.AllowWeakCrypto)
   312  	l.DefaultTktEnctypeIDs = parseETypes(l.DefaultTktEnctypes, l.AllowWeakCrypto)
   313  	l.PermittedEnctypeIDs = parseETypes(l.PermittedEnctypes, l.AllowWeakCrypto)
   314  	return nil
   315  }
   316  
   317  // Realm represents an entry in the [realms] section of the configuration.
   318  type Realm struct {
   319  	Realm       string
   320  	AdminServer []string
   321  	//auth_to_local //Not implementing for now
   322  	//auth_to_local_names //Not implementing for now
   323  	DefaultDomain string
   324  	KDC           []string
   325  	KPasswdServer []string //default admin_server:464
   326  	MasterKDC     []string
   327  }
   328  
   329  // Parse the lines of a [realms] entry into the Realm struct.
   330  func (r *Realm) parseLines(name string, lines []string) (err error) {
   331  	r.Realm = name
   332  	var adminServerFinal bool
   333  	var KDCFinal bool
   334  	var kpasswdServerFinal bool
   335  	var masterKDCFinal bool
   336  	var ignore bool
   337  	var c int // counts the depth of blocks within brackets { }
   338  	for _, line := range lines {
   339  		if ignore && c > 0 && !strings.Contains(line, "{") && !strings.Contains(line, "}") {
   340  			continue
   341  		}
   342  		//Remove comments after the values
   343  		if idx := strings.IndexAny(line, "#;"); idx != -1 {
   344  			line = line[:idx]
   345  		}
   346  		line = strings.TrimSpace(line)
   347  		if line == "" {
   348  			continue
   349  		}
   350  		if !strings.Contains(line, "=") && !strings.Contains(line, "}") {
   351  			return InvalidErrorf("realms section line (%s)", line)
   352  		}
   353  		if strings.Contains(line, "v4_") {
   354  			ignore = true
   355  			err = UnsupportedDirective{"v4 configurations are not supported"}
   356  		}
   357  		if strings.Contains(line, "{") {
   358  			c++
   359  			if ignore {
   360  				continue
   361  			}
   362  		}
   363  		if strings.Contains(line, "}") {
   364  			c--
   365  			if c < 0 {
   366  				return InvalidErrorf("unpaired curly brackets")
   367  			}
   368  			if ignore {
   369  				if c < 1 {
   370  					c = 0
   371  					ignore = false
   372  				}
   373  				continue
   374  			}
   375  		}
   376  
   377  		p := strings.Split(line, "=")
   378  		key := strings.TrimSpace(strings.ToLower(p[0]))
   379  		v := strings.TrimSpace(p[1])
   380  		switch key {
   381  		case "admin_server":
   382  			appendUntilFinal(&r.AdminServer, v, &adminServerFinal)
   383  		case "default_domain":
   384  			r.DefaultDomain = v
   385  		case "kdc":
   386  			if !strings.Contains(v, ":") {
   387  				// No port number specified default to 88
   388  				if strings.HasSuffix(v, `*`) {
   389  					v = strings.TrimSpace(strings.TrimSuffix(v, `*`)) + ":88*"
   390  				} else {
   391  					v = strings.TrimSpace(v) + ":88"
   392  				}
   393  			}
   394  			appendUntilFinal(&r.KDC, v, &KDCFinal)
   395  		case "kpasswd_server":
   396  			appendUntilFinal(&r.KPasswdServer, v, &kpasswdServerFinal)
   397  		case "master_kdc":
   398  			appendUntilFinal(&r.MasterKDC, v, &masterKDCFinal)
   399  		}
   400  	}
   401  	//default for Kpasswd_server = admin_server:464
   402  	if len(r.KPasswdServer) < 1 {
   403  		for _, a := range r.AdminServer {
   404  			s := strings.Split(a, ":")
   405  			r.KPasswdServer = append(r.KPasswdServer, s[0]+":464")
   406  		}
   407  	}
   408  	return
   409  }
   410  
   411  // Parse the lines of the [realms] section of the configuration into an slice of Realm structs.
   412  func parseRealms(lines []string) (realms []Realm, err error) {
   413  	var name string
   414  	var start int
   415  	var c int
   416  	for i, l := range lines {
   417  		//Remove comments after the values
   418  		if idx := strings.IndexAny(l, "#;"); idx != -1 {
   419  			l = l[:idx]
   420  		}
   421  		l = strings.TrimSpace(l)
   422  		if l == "" {
   423  			continue
   424  		}
   425  		//if strings.Contains(l, "v4_") {
   426  		//	return nil, errors.New("v4 configurations are not supported in Realms section")
   427  		//}
   428  		if strings.Contains(l, "{") {
   429  			c++
   430  			if !strings.Contains(l, "=") {
   431  				return nil, fmt.Errorf("realm configuration line invalid: %s", l)
   432  			}
   433  			if c == 1 {
   434  				start = i
   435  				p := strings.Split(l, "=")
   436  				name = strings.TrimSpace(p[0])
   437  			}
   438  		}
   439  		if strings.Contains(l, "}") {
   440  			if c < 1 {
   441  				// but not started a block!!!
   442  				return nil, errors.New("invalid Realms section in configuration")
   443  			}
   444  			c--
   445  			if c == 0 {
   446  				var r Realm
   447  				e := r.parseLines(name, lines[start+1:i])
   448  				if e != nil {
   449  					if _, ok := e.(UnsupportedDirective); !ok {
   450  						err = e
   451  						return
   452  					}
   453  					err = e
   454  				}
   455  				realms = append(realms, r)
   456  			}
   457  		}
   458  	}
   459  	return
   460  }
   461  
   462  // DomainRealm maps the domains to realms representing the [domain_realm] section of the configuration.
   463  type DomainRealm map[string]string
   464  
   465  // Parse the lines of the [domain_realm] section of the configuration and add to the mapping.
   466  func (d *DomainRealm) parseLines(lines []string) error {
   467  	for _, line := range lines {
   468  		//Remove comments after the values
   469  		if idx := strings.IndexAny(line, "#;"); idx != -1 {
   470  			line = line[:idx]
   471  		}
   472  		if strings.TrimSpace(line) == "" {
   473  			continue
   474  		}
   475  		if !strings.Contains(line, "=") {
   476  			return InvalidErrorf("realm line (%s)", line)
   477  		}
   478  		p := strings.Split(line, "=")
   479  		domain := strings.TrimSpace(strings.ToLower(p[0]))
   480  		realm := strings.TrimSpace(p[1])
   481  		d.addMapping(domain, realm)
   482  	}
   483  	return nil
   484  }
   485  
   486  // Add a domain to realm mapping.
   487  func (d *DomainRealm) addMapping(domain, realm string) {
   488  	(*d)[domain] = realm
   489  }
   490  
   491  // Delete a domain to realm mapping.
   492  func (d *DomainRealm) deleteMapping(domain, realm string) {
   493  	delete(*d, domain)
   494  }
   495  
   496  // ResolveRealm resolves the kerberos realm for the specified domain name from the domain to realm mapping.
   497  // The most specific mapping is returned.
   498  func (c *Config) ResolveRealm(domainName string) string {
   499  	domainName = strings.TrimSuffix(domainName, ".")
   500  
   501  	// Try to match the entire hostname first
   502  	if r, ok := c.DomainRealm[domainName]; ok {
   503  		return r
   504  	}
   505  
   506  	// Try to match all DNS domain parts
   507  	periods := strings.Count(domainName, ".") + 1
   508  	for i := 2; i <= periods; i++ {
   509  		z := strings.SplitN(domainName, ".", i)
   510  		if r, ok := c.DomainRealm["."+z[len(z)-1]]; ok {
   511  			return r
   512  		}
   513  	}
   514  	return ""
   515  }
   516  
   517  // Load the KRB5 configuration from the specified file path.
   518  func Load(cfgPath string) (*Config, error) {
   519  	fh, err := os.Open(cfgPath)
   520  	if err != nil {
   521  		return nil, errors.New("configuration file could not be opened: " + cfgPath + " " + err.Error())
   522  	}
   523  	defer fh.Close()
   524  	scanner := bufio.NewScanner(fh)
   525  	return NewFromScanner(scanner)
   526  }
   527  
   528  // NewFromString creates a new Config struct from a string.
   529  func NewFromString(s string) (*Config, error) {
   530  	reader := strings.NewReader(s)
   531  	return NewFromReader(reader)
   532  }
   533  
   534  // NewFromReader creates a new Config struct from an io.Reader.
   535  func NewFromReader(r io.Reader) (*Config, error) {
   536  	scanner := bufio.NewScanner(r)
   537  	return NewFromScanner(scanner)
   538  }
   539  
   540  // NewFromScanner creates a new Config struct from a bufio.Scanner.
   541  func NewFromScanner(scanner *bufio.Scanner) (*Config, error) {
   542  	c := New()
   543  	var e error
   544  	sections := make(map[int]string)
   545  	var sectionLineNum []int
   546  	var lines []string
   547  	for scanner.Scan() {
   548  		// Skip comments and blank lines
   549  		if matched, _ := regexp.MatchString(`^\s*(#|;|\n)`, scanner.Text()); matched {
   550  			continue
   551  		}
   552  		if matched, _ := regexp.MatchString(`^\s*\[libdefaults\]\s*`, scanner.Text()); matched {
   553  			sections[len(lines)] = "libdefaults"
   554  			sectionLineNum = append(sectionLineNum, len(lines))
   555  			continue
   556  		}
   557  		if matched, _ := regexp.MatchString(`^\s*\[realms\]\s*`, scanner.Text()); matched {
   558  			sections[len(lines)] = "realms"
   559  			sectionLineNum = append(sectionLineNum, len(lines))
   560  			continue
   561  		}
   562  		if matched, _ := regexp.MatchString(`^\s*\[domain_realm\]\s*`, scanner.Text()); matched {
   563  			sections[len(lines)] = "domain_realm"
   564  			sectionLineNum = append(sectionLineNum, len(lines))
   565  			continue
   566  		}
   567  		if matched, _ := regexp.MatchString(`^\s*\[.*\]\s*`, scanner.Text()); matched {
   568  			sections[len(lines)] = "unknown_section"
   569  			sectionLineNum = append(sectionLineNum, len(lines))
   570  			continue
   571  		}
   572  		lines = append(lines, scanner.Text())
   573  	}
   574  	for i, start := range sectionLineNum {
   575  		var end int
   576  		if i+1 >= len(sectionLineNum) {
   577  			end = len(lines)
   578  		} else {
   579  			end = sectionLineNum[i+1]
   580  		}
   581  		switch section := sections[start]; section {
   582  		case "libdefaults":
   583  			err := c.LibDefaults.parseLines(lines[start:end])
   584  			if err != nil {
   585  				if _, ok := err.(UnsupportedDirective); !ok {
   586  					return nil, fmt.Errorf("error processing libdefaults section: %v", err)
   587  				}
   588  				e = err
   589  			}
   590  		case "realms":
   591  			realms, err := parseRealms(lines[start:end])
   592  			if err != nil {
   593  				if _, ok := err.(UnsupportedDirective); !ok {
   594  					return nil, fmt.Errorf("error processing realms section: %v", err)
   595  				}
   596  				e = err
   597  			}
   598  			c.Realms = realms
   599  		case "domain_realm":
   600  			err := c.DomainRealm.parseLines(lines[start:end])
   601  			if err != nil {
   602  				if _, ok := err.(UnsupportedDirective); !ok {
   603  					return nil, fmt.Errorf("error processing domaain_realm section: %v", err)
   604  				}
   605  				e = err
   606  			}
   607  		}
   608  	}
   609  	return c, e
   610  }
   611  
   612  // Parse a space delimited list of ETypes into a list of EType numbers optionally filtering out weak ETypes.
   613  func parseETypes(s []string, w bool) []int32 {
   614  	var eti []int32
   615  	for _, et := range s {
   616  		if !w {
   617  			var weak bool
   618  			for _, wet := range strings.Fields(WeakETypeList) {
   619  				if et == wet {
   620  					weak = true
   621  					break
   622  				}
   623  			}
   624  			if weak {
   625  				continue
   626  			}
   627  		}
   628  		i := etypeID.EtypeSupported(et)
   629  		if i != 0 {
   630  			eti = append(eti, i)
   631  		}
   632  	}
   633  	return eti
   634  }
   635  
   636  // Parse a time duration string in the configuration to a golang time.Duration.
   637  func parseDuration(s string) (time.Duration, error) {
   638  	s = strings.Replace(strings.TrimSpace(s), " ", "", -1)
   639  
   640  	// handle Nd[NmNs]
   641  	if strings.Contains(s, "d") {
   642  		ds := strings.SplitN(s, "d", 2)
   643  		dn, err := strconv.ParseUint(ds[0], 10, 32)
   644  		if err != nil {
   645  			return time.Duration(0), errors.New("invalid time duration")
   646  		}
   647  		d := time.Duration(dn*24) * time.Hour
   648  		if ds[1] != "" {
   649  			dp, err := time.ParseDuration(ds[1])
   650  			if err != nil {
   651  				return time.Duration(0), errors.New("invalid time duration")
   652  			}
   653  			d = d + dp
   654  		}
   655  		return d, nil
   656  	}
   657  
   658  	// handle Nm[Ns]
   659  	d, err := time.ParseDuration(s)
   660  	if err == nil {
   661  		return d, nil
   662  	}
   663  
   664  	// handle N
   665  	v, err := strconv.ParseUint(s, 10, 32)
   666  	if err == nil && v > 0 {
   667  		return time.Duration(v) * time.Second, nil
   668  	}
   669  
   670  	// handle h:m[:s]
   671  	if strings.Contains(s, ":") {
   672  		t := strings.Split(s, ":")
   673  		if 2 > len(t) || len(t) > 3 {
   674  			return time.Duration(0), errors.New("invalid time duration value")
   675  		}
   676  		var i []int
   677  		for _, n := range t {
   678  			j, err := strconv.ParseInt(n, 10, 16)
   679  			if err != nil {
   680  				return time.Duration(0), errors.New("invalid time duration value")
   681  			}
   682  			i = append(i, int(j))
   683  		}
   684  		d := time.Duration(i[0])*time.Hour + time.Duration(i[1])*time.Minute
   685  		if len(i) == 3 {
   686  			d = d + time.Duration(i[2])*time.Second
   687  		}
   688  		return d, nil
   689  	}
   690  	return time.Duration(0), errors.New("invalid time duration value")
   691  }
   692  
   693  // Parse possible boolean values to golang bool.
   694  func parseBoolean(s string) (bool, error) {
   695  	s = strings.TrimSpace(s)
   696  	v, err := strconv.ParseBool(s)
   697  	if err == nil {
   698  		return v, nil
   699  	}
   700  	switch strings.ToLower(s) {
   701  	case "yes":
   702  		return true, nil
   703  	case "y":
   704  		return true, nil
   705  	case "no":
   706  		return false, nil
   707  	case "n":
   708  		return false, nil
   709  	}
   710  	return false, errors.New("invalid boolean value")
   711  }
   712  
   713  // Parse array of strings but stop if an asterisk is placed at the end of a line.
   714  func appendUntilFinal(s *[]string, value string, final *bool) {
   715  	if *final {
   716  		return
   717  	}
   718  	if last := len(value) - 1; last >= 0 && value[last] == '*' {
   719  		*final = true
   720  		value = value[:len(value)-1]
   721  	}
   722  	*s = append(*s, value)
   723  }
   724  
   725  // JSON return details of the config in a JSON format.
   726  func (c *Config) JSON() (string, error) {
   727  	b, err := json.MarshalIndent(c, "", "  ")
   728  	if err != nil {
   729  		return "", err
   730  	}
   731  	return string(b), nil
   732  }