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