package config
import (
"bufio"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"os/user"
"regexp"
"strconv"
"strings"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
)
type Config struct {
LibDefaults LibDefaults
Realms []Realm
DomainRealm DomainRealm
}
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"
func New () *Config {
d := make (DomainRealm )
return &Config {
LibDefaults : newLibDefaults (),
DomainRealm : d ,
}
}
type LibDefaults struct {
AllowWeakCrypto bool
Canonicalize bool
CCacheType int
Clockskew time .Duration
DefaultClientKeytabName string
DefaultKeytabName string
DefaultRealm string
DefaultTGSEnctypes []string
DefaultTktEnctypes []string
DefaultTGSEnctypeIDs []int32
DefaultTktEnctypeIDs []int32
DNSCanonicalizeHostname bool
DNSLookupKDC bool
DNSLookupRealm bool
ExtraAddresses []net .IP
Forwardable bool
IgnoreAcceptorHostname bool
K5LoginAuthoritative bool
K5LoginDirectory string
KDCDefaultOptions asn1 .BitString
KDCTimeSync int
NoAddresses bool
PermittedEnctypes []string
PermittedEnctypeIDs []int32
PreferredPreauthTypes []int
Proxiable bool
RDNS bool
RealmTryDomains int
RenewLifetime time .Duration
SafeChecksumType int
TicketLifetime time .Duration
UDPPreferenceLimit int
VerifyAPReqNofail bool
}
func newLibDefaults() LibDefaults {
uid := "0"
var hdir string
usr , _ := user .Current ()
if usr != nil {
uid = usr .Uid
hdir = usr .HomeDir
}
opts := asn1 .BitString {}
opts .Bytes , _ = hex .DecodeString ("00000010" )
opts .BitLength = len (opts .Bytes ) * 8
l := LibDefaults {
CCacheType : 4 ,
Clockskew : time .Duration (300 ) * time .Second ,
DefaultClientKeytabName : fmt .Sprintf ("/usr/local/var/krb5/user/%s/client.keytab" , uid ),
DefaultKeytabName : "/etc/krb5.keytab" ,
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" },
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" },
DNSCanonicalizeHostname : true ,
K5LoginDirectory : hdir ,
KDCDefaultOptions : opts ,
KDCTimeSync : 1 ,
NoAddresses : true ,
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" },
RDNS : true ,
RealmTryDomains : -1 ,
SafeChecksumType : 8 ,
TicketLifetime : time .Duration (24 ) * time .Hour ,
UDPPreferenceLimit : 1465 ,
PreferredPreauthTypes : []int {17 , 16 , 15 , 14 },
}
l .DefaultTGSEnctypeIDs = parseETypes (l .DefaultTGSEnctypes , l .AllowWeakCrypto )
l .DefaultTktEnctypeIDs = parseETypes (l .DefaultTktEnctypes , l .AllowWeakCrypto )
l .PermittedEnctypeIDs = parseETypes (l .PermittedEnctypes , l .AllowWeakCrypto )
return l
}
func (l *LibDefaults ) parseLines (lines []string ) error {
for _ , line := range lines {
if idx := strings .IndexAny (line , "#;" ); idx != -1 {
line = line [:idx ]
}
line = strings .TrimSpace (line )
if line == "" {
continue
}
if !strings .Contains (line , "=" ) {
return InvalidErrorf ("libdefaults section line (%s)" , line )
}
p := strings .Split (line , "=" )
key := strings .TrimSpace (strings .ToLower (p [0 ]))
switch key {
case "allow_weak_crypto" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .AllowWeakCrypto = v
case "canonicalize" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .Canonicalize = v
case "ccache_type" :
p [1 ] = strings .TrimSpace (p [1 ])
v , err := strconv .ParseUint (p [1 ], 10 , 32 )
if err != nil || v < 0 || v > 4 {
return InvalidErrorf ("libdefaults section line (%s)" , line )
}
l .CCacheType = int (v )
case "clockskew" :
d , err := parseDuration (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .Clockskew = d
case "default_client_keytab_name" :
l .DefaultClientKeytabName = strings .TrimSpace (p [1 ])
case "default_keytab_name" :
l .DefaultKeytabName = strings .TrimSpace (p [1 ])
case "default_realm" :
l .DefaultRealm = strings .TrimSpace (p [1 ])
case "default_tgs_enctypes" :
l .DefaultTGSEnctypes = strings .Fields (p [1 ])
case "default_tkt_enctypes" :
l .DefaultTktEnctypes = strings .Fields (p [1 ])
case "dns_canonicalize_hostname" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .DNSCanonicalizeHostname = v
case "dns_lookup_kdc" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .DNSLookupKDC = v
case "dns_lookup_realm" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .DNSLookupRealm = v
case "extra_addresses" :
ipStr := strings .TrimSpace (p [1 ])
for _ , ip := range strings .Split (ipStr , "," ) {
if eip := net .ParseIP (ip ); eip != nil {
l .ExtraAddresses = append (l .ExtraAddresses , eip )
}
}
case "forwardable" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .Forwardable = v
case "ignore_acceptor_hostname" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .IgnoreAcceptorHostname = v
case "k5login_authoritative" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .K5LoginAuthoritative = v
case "k5login_directory" :
l .K5LoginDirectory = strings .TrimSpace (p [1 ])
case "kdc_default_options" :
v := strings .TrimSpace (p [1 ])
v = strings .Replace (v , "0x" , "" , -1 )
b , err := hex .DecodeString (v )
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .KDCDefaultOptions .Bytes = b
l .KDCDefaultOptions .BitLength = len (b ) * 8
case "kdc_timesync" :
p [1 ] = strings .TrimSpace (p [1 ])
v , err := strconv .ParseInt (p [1 ], 10 , 32 )
if err != nil || v < 0 {
return InvalidErrorf ("libdefaults section line (%s)" , line )
}
l .KDCTimeSync = int (v )
case "noaddresses" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .NoAddresses = v
case "permitted_enctypes" :
l .PermittedEnctypes = strings .Fields (p [1 ])
case "preferred_preauth_types" :
p [1 ] = strings .TrimSpace (p [1 ])
t := strings .Split (p [1 ], "," )
var v []int
for _ , s := range t {
i , err := strconv .ParseInt (s , 10 , 32 )
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
v = append (v , int (i ))
}
l .PreferredPreauthTypes = v
case "proxiable" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .Proxiable = v
case "rdns" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .RDNS = v
case "realm_try_domains" :
p [1 ] = strings .TrimSpace (p [1 ])
v , err := strconv .ParseInt (p [1 ], 10 , 32 )
if err != nil || v < -1 {
return InvalidErrorf ("libdefaults section line (%s)" , line )
}
l .RealmTryDomains = int (v )
case "renew_lifetime" :
d , err := parseDuration (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .RenewLifetime = d
case "safe_checksum_type" :
p [1 ] = strings .TrimSpace (p [1 ])
v , err := strconv .ParseInt (p [1 ], 10 , 32 )
if err != nil || v < 0 {
return InvalidErrorf ("libdefaults section line (%s)" , line )
}
l .SafeChecksumType = int (v )
case "ticket_lifetime" :
d , err := parseDuration (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .TicketLifetime = d
case "udp_preference_limit" :
p [1 ] = strings .TrimSpace (p [1 ])
v , err := strconv .ParseUint (p [1 ], 10 , 32 )
if err != nil || v > 32700 {
return InvalidErrorf ("libdefaults section line (%s)" , line )
}
l .UDPPreferenceLimit = int (v )
case "verify_ap_req_nofail" :
v , err := parseBoolean (p [1 ])
if err != nil {
return InvalidErrorf ("libdefaults section line (%s): %v" , line , err )
}
l .VerifyAPReqNofail = v
}
}
l .DefaultTGSEnctypeIDs = parseETypes (l .DefaultTGSEnctypes , l .AllowWeakCrypto )
l .DefaultTktEnctypeIDs = parseETypes (l .DefaultTktEnctypes , l .AllowWeakCrypto )
l .PermittedEnctypeIDs = parseETypes (l .PermittedEnctypes , l .AllowWeakCrypto )
return nil
}
type Realm struct {
Realm string
AdminServer []string
DefaultDomain string
KDC []string
KPasswdServer []string
MasterKDC []string
}
func (r *Realm ) parseLines (name string , lines []string ) (err error ) {
r .Realm = name
var adminServerFinal bool
var KDCFinal bool
var kpasswdServerFinal bool
var masterKDCFinal bool
var ignore bool
var c int
for _ , line := range lines {
if ignore && c > 0 && !strings .Contains (line , "{" ) && !strings .Contains (line , "}" ) {
continue
}
if idx := strings .IndexAny (line , "#;" ); idx != -1 {
line = line [:idx ]
}
line = strings .TrimSpace (line )
if line == "" {
continue
}
if !strings .Contains (line , "=" ) && !strings .Contains (line , "}" ) {
return InvalidErrorf ("realms section line (%s)" , line )
}
if strings .Contains (line , "v4_" ) {
ignore = true
err = UnsupportedDirective {"v4 configurations are not supported" }
}
if strings .Contains (line , "{" ) {
c ++
if ignore {
continue
}
}
if strings .Contains (line , "}" ) {
c --
if c < 0 {
return InvalidErrorf ("unpaired curly brackets" )
}
if ignore {
if c < 1 {
c = 0
ignore = false
}
continue
}
}
p := strings .Split (line , "=" )
key := strings .TrimSpace (strings .ToLower (p [0 ]))
v := strings .TrimSpace (p [1 ])
switch key {
case "admin_server" :
appendUntilFinal (&r .AdminServer , v , &adminServerFinal )
case "default_domain" :
r .DefaultDomain = v
case "kdc" :
if !strings .Contains (v , ":" ) {
if strings .HasSuffix (v , `*` ) {
v = strings .TrimSpace (strings .TrimSuffix (v , `*` )) + ":88*"
} else {
v = strings .TrimSpace (v ) + ":88"
}
}
appendUntilFinal (&r .KDC , v , &KDCFinal )
case "kpasswd_server" :
appendUntilFinal (&r .KPasswdServer , v , &kpasswdServerFinal )
case "master_kdc" :
appendUntilFinal (&r .MasterKDC , v , &masterKDCFinal )
}
}
if len (r .KPasswdServer ) < 1 {
for _ , a := range r .AdminServer {
s := strings .Split (a , ":" )
r .KPasswdServer = append (r .KPasswdServer , s [0 ]+":464" )
}
}
return
}
func parseRealms(lines []string ) (realms []Realm , err error ) {
var name string
var start int
var c int
for i , l := range lines {
if idx := strings .IndexAny (l , "#;" ); idx != -1 {
l = l [:idx ]
}
l = strings .TrimSpace (l )
if l == "" {
continue
}
if strings .Contains (l , "{" ) {
c ++
if !strings .Contains (l , "=" ) {
return nil , fmt .Errorf ("realm configuration line invalid: %s" , l )
}
if c == 1 {
start = i
p := strings .Split (l , "=" )
name = strings .TrimSpace (p [0 ])
}
}
if strings .Contains (l , "}" ) {
if c < 1 {
return nil , errors .New ("invalid Realms section in configuration" )
}
c --
if c == 0 {
var r Realm
e := r .parseLines (name , lines [start +1 :i ])
if e != nil {
if _ , ok := e .(UnsupportedDirective ); !ok {
err = e
return
}
err = e
}
realms = append (realms , r )
}
}
}
return
}
type DomainRealm map [string ]string
func (d *DomainRealm ) parseLines (lines []string ) error {
for _ , line := range lines {
if idx := strings .IndexAny (line , "#;" ); idx != -1 {
line = line [:idx ]
}
if strings .TrimSpace (line ) == "" {
continue
}
if !strings .Contains (line , "=" ) {
return InvalidErrorf ("realm line (%s)" , line )
}
p := strings .Split (line , "=" )
domain := strings .TrimSpace (strings .ToLower (p [0 ]))
realm := strings .TrimSpace (p [1 ])
d .addMapping (domain , realm )
}
return nil
}
func (d *DomainRealm ) addMapping (domain , realm string ) {
(*d )[domain ] = realm
}
func (d *DomainRealm ) deleteMapping (domain , realm string ) {
delete (*d , domain )
}
func (c *Config ) ResolveRealm (domainName string ) string {
domainName = strings .TrimSuffix (domainName , "." )
if r , ok := c .DomainRealm [domainName ]; ok {
return r
}
periods := strings .Count (domainName , "." ) + 1
for i := 2 ; i <= periods ; i ++ {
z := strings .SplitN (domainName , "." , i )
if r , ok := c .DomainRealm ["." +z [len (z )-1 ]]; ok {
return r
}
}
return ""
}
func Load (cfgPath string ) (*Config , error ) {
fh , err := os .Open (cfgPath )
if err != nil {
return nil , errors .New ("configuration file could not be opened: " + cfgPath + " " + err .Error())
}
defer fh .Close ()
scanner := bufio .NewScanner (fh )
return NewFromScanner (scanner )
}
func NewFromString (s string ) (*Config , error ) {
reader := strings .NewReader (s )
return NewFromReader (reader )
}
func NewFromReader (r io .Reader ) (*Config , error ) {
scanner := bufio .NewScanner (r )
return NewFromScanner (scanner )
}
func NewFromScanner (scanner *bufio .Scanner ) (*Config , error ) {
c := New ()
var e error
sections := make (map [int ]string )
var sectionLineNum []int
var lines []string
for scanner .Scan () {
if matched , _ := regexp .MatchString (`^\s*(#|;|\n)` , scanner .Text ()); matched {
continue
}
if matched , _ := regexp .MatchString (`^\s*\[libdefaults\]\s*` , scanner .Text ()); matched {
sections [len (lines )] = "libdefaults"
sectionLineNum = append (sectionLineNum , len (lines ))
continue
}
if matched , _ := regexp .MatchString (`^\s*\[realms\]\s*` , scanner .Text ()); matched {
sections [len (lines )] = "realms"
sectionLineNum = append (sectionLineNum , len (lines ))
continue
}
if matched , _ := regexp .MatchString (`^\s*\[domain_realm\]\s*` , scanner .Text ()); matched {
sections [len (lines )] = "domain_realm"
sectionLineNum = append (sectionLineNum , len (lines ))
continue
}
if matched , _ := regexp .MatchString (`^\s*\[.*\]\s*` , scanner .Text ()); matched {
sections [len (lines )] = "unknown_section"
sectionLineNum = append (sectionLineNum , len (lines ))
continue
}
lines = append (lines , scanner .Text ())
}
for i , start := range sectionLineNum {
var end int
if i +1 >= len (sectionLineNum ) {
end = len (lines )
} else {
end = sectionLineNum [i +1 ]
}
switch section := sections [start ]; section {
case "libdefaults" :
err := c .LibDefaults .parseLines (lines [start :end ])
if err != nil {
if _ , ok := err .(UnsupportedDirective ); !ok {
return nil , fmt .Errorf ("error processing libdefaults section: %v" , err )
}
e = err
}
case "realms" :
realms , err := parseRealms (lines [start :end ])
if err != nil {
if _ , ok := err .(UnsupportedDirective ); !ok {
return nil , fmt .Errorf ("error processing realms section: %v" , err )
}
e = err
}
c .Realms = realms
case "domain_realm" :
err := c .DomainRealm .parseLines (lines [start :end ])
if err != nil {
if _ , ok := err .(UnsupportedDirective ); !ok {
return nil , fmt .Errorf ("error processing domaain_realm section: %v" , err )
}
e = err
}
}
}
return c , e
}
func parseETypes(s []string , w bool ) []int32 {
var eti []int32
for _ , et := range s {
if !w {
var weak bool
for _ , wet := range strings .Fields (WeakETypeList ) {
if et == wet {
weak = true
break
}
}
if weak {
continue
}
}
i := etypeID .EtypeSupported (et )
if i != 0 {
eti = append (eti , i )
}
}
return eti
}
func parseDuration(s string ) (time .Duration , error ) {
s = strings .Replace (strings .TrimSpace (s ), " " , "" , -1 )
if strings .Contains (s , "d" ) {
ds := strings .SplitN (s , "d" , 2 )
dn , err := strconv .ParseUint (ds [0 ], 10 , 32 )
if err != nil {
return time .Duration (0 ), errors .New ("invalid time duration" )
}
d := time .Duration (dn *24 ) * time .Hour
if ds [1 ] != "" {
dp , err := time .ParseDuration (ds [1 ])
if err != nil {
return time .Duration (0 ), errors .New ("invalid time duration" )
}
d = d + dp
}
return d , nil
}
d , err := time .ParseDuration (s )
if err == nil {
return d , nil
}
v , err := strconv .ParseUint (s , 10 , 32 )
if err == nil && v > 0 {
return time .Duration (v ) * time .Second , nil
}
if strings .Contains (s , ":" ) {
t := strings .Split (s , ":" )
if 2 > len (t ) || len (t ) > 3 {
return time .Duration (0 ), errors .New ("invalid time duration value" )
}
var i []int
for _ , n := range t {
j , err := strconv .ParseInt (n , 10 , 16 )
if err != nil {
return time .Duration (0 ), errors .New ("invalid time duration value" )
}
i = append (i , int (j ))
}
d := time .Duration (i [0 ])*time .Hour + time .Duration (i [1 ])*time .Minute
if len (i ) == 3 {
d = d + time .Duration (i [2 ])*time .Second
}
return d , nil
}
return time .Duration (0 ), errors .New ("invalid time duration value" )
}
func parseBoolean(s string ) (bool , error ) {
s = strings .TrimSpace (s )
v , err := strconv .ParseBool (s )
if err == nil {
return v , nil
}
switch strings .ToLower (s ) {
case "yes" :
return true , nil
case "y" :
return true , nil
case "no" :
return false , nil
case "n" :
return false , nil
}
return false , errors .New ("invalid boolean value" )
}
func appendUntilFinal(s *[]string , value string , final *bool ) {
if *final {
return
}
if last := len (value ) - 1 ; last >= 0 && value [last ] == '*' {
*final = true
value = value [:len (value )-1 ]
}
*s = append (*s , value )
}
func (c *Config ) JSON () (string , error ) {
b , err := json .MarshalIndent (c , "" , " " )
if err != nil {
return "" , err
}
return string (b ), nil
}
The pages are generated with Golds v0.6.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .