github.com/oarkflow/log@v1.0.78/fqdn/fqdn.go (about) 1 package fqdn 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "net" 8 "os" 9 ) 10 11 // isalnum(3p) in POSIX locale 12 func isalnum(r rune) bool { 13 return (r >= 'a' && r <= 'z') || 14 (r >= 'A' && r <= 'Z') || 15 (r >= '0' && r <= '9') 16 } 17 18 const ( 19 maxHostnameLen = 254 20 ) 21 22 // Validate hostname, based on musl-c version of this function. 23 func isValidHostname(s string) bool { 24 if len(s) > maxHostnameLen { 25 return false 26 } 27 28 for _, c := range s { 29 if !(c >= 0x80 || c == '.' || c == '-' || isalnum(c)) { 30 return false 31 } 32 } 33 34 return true 35 } 36 37 func parseHostLine(host string, line string) (string, bool) { 38 const ( 39 StateSkipWhite = iota 40 StateIp 41 StateCanonFirst 42 StateCanon 43 StateAliasFirst 44 StateAlias 45 ) 46 47 var ( 48 canon string 49 state int 50 nextState int 51 52 i int 53 start int 54 ) 55 56 isWhite := func(b byte) bool { 57 return b == ' ' || b == '\t' 58 } 59 isLast := func() bool { 60 return i == len(line)-1 || isWhite(line[i+1]) 61 } 62 partStr := func() string { 63 return line[start : i+1] 64 } 65 66 state = StateSkipWhite 67 nextState = StateIp 68 69 debug("Looking for %q in %q", host, line) 70 for i = 0; i < len(line); i += 1 { 71 debug("%03d: character %q, state: %d, nstate: %d", 72 i, line[i], state, nextState) 73 74 if line[i] == '#' { 75 debug("%03d: found comment, terminating", i) 76 break 77 } 78 79 switch state { 80 case StateSkipWhite: 81 if !isWhite(line[i]) { 82 state = nextState 83 i -= 1 84 } 85 case StateIp: 86 if isLast() { 87 state = StateSkipWhite 88 nextState = StateCanonFirst 89 } 90 case StateCanonFirst: 91 start = i 92 state = StateCanon 93 i -= 1 94 case StateCanon: 95 debug("Canon so far: %q", partStr()) 96 if isLast() { 97 canon = partStr() 98 if !isValidHostname(canon) { 99 return "", false 100 } 101 102 if canon == host { 103 debug("Canon match") 104 return canon, true 105 } 106 107 state = StateSkipWhite 108 nextState = StateAliasFirst 109 } 110 case StateAliasFirst: 111 start = i 112 state = StateAlias 113 i -= 1 114 case StateAlias: 115 debug("Alias so far: %q", partStr()) 116 if isLast() { 117 alias := partStr() 118 if alias == host { 119 debug("Alias match") 120 return canon, true 121 } 122 123 state = StateSkipWhite 124 nextState = StateAliasFirst 125 } 126 default: 127 panic(fmt.Sprintf("BUG: State not handled: %d", state)) 128 } 129 } 130 131 debug("No match") 132 return "", false 133 } 134 135 // Reads hosts(5) file and tries to get canonical name for host. 136 func fromHosts(host string) (string, error) { 137 var ( 138 fqdn string 139 line string 140 err error 141 file *os.File 142 r *bufio.Reader 143 ok bool 144 ) 145 146 file, err = os.Open(hostsPath) 147 if err != nil { 148 err = fmt.Errorf("cannot open hosts file: %w", err) 149 goto out 150 } 151 defer file.Close() 152 153 r = bufio.NewReader(file) 154 for line, err = readline(r); err == nil; line, err = readline(r) { 155 fqdn, ok = parseHostLine(host, line) 156 if ok { 157 goto out 158 } 159 } 160 161 if err != io.EOF { 162 err = fmt.Errorf("failed to read file: %w", err) 163 goto out 164 } 165 err = errFqdnNotFound{} 166 167 out: 168 return fqdn, err 169 } 170 171 func fromLookup(host string) (string, error) { 172 var ( 173 fqdn string 174 err error 175 addrs []net.IP 176 hosts []string 177 ) 178 179 fqdn, err = net.LookupCNAME(host) 180 if err == nil && len(fqdn) != 0 { 181 debug("LookupCNAME success: %q", fqdn) 182 goto out 183 } 184 debug("LookupCNAME failed: %v", err) 185 186 debug("Looking up: %q", host) 187 addrs, err = net.LookupIP(host) 188 if err != nil { 189 err = errFqdnNotFound{err} 190 goto out 191 } 192 debug("Resolved addrs: %q", addrs) 193 194 for _, addr := range addrs { 195 debug("Trying: %q", addr) 196 hosts, err = net.LookupAddr(addr.String()) 197 // On windows it can return err == nil but empty list of hosts 198 if err != nil || len(hosts) == 0 { 199 continue 200 } 201 debug("Resolved hosts: %q", hosts) 202 203 // First one should be the canonical hostname 204 fqdn = hosts[0] 205 206 goto out 207 } 208 209 err = errFqdnNotFound{} 210 out: 211 // For some reason we wanted the canonical hostname without 212 // trailing dot. So if it is present, strip it. 213 if len(fqdn) > 0 && fqdn[len(fqdn)-1] == '.' { 214 fqdn = fqdn[:len(fqdn)-1] 215 } 216 217 return fqdn, err 218 } 219 220 // Hostname Try to get fully qualified hostname for current machine. 221 // 222 // It tries to mimic how `hostname -f` works, so except for few edge cases you 223 // should get the same result from both. One thing that needs to be mentioned is 224 // that it does not guarantee that you get back fqdn. There is no way to do that 225 // and `hostname -f` can also return non-fqdn hostname if your /etc/hosts is 226 // fucked up. 227 // 228 // It checks few sources in this order: 229 // 230 // 1. hosts file 231 // It parses hosts file if present and readable and returns first canonical 232 // hostname that also references your hostname. See hosts(5) for more 233 // details. 234 // 2. dns lookup 235 // If lookup in hosts file fails, it tries to ask dns. 236 // 237 // If none of steps above succeeds, ErrFqdnNotFound is returned as error. You 238 // will probably want to just use output from os.Hostname() at that point. 239 func Hostname() (string, error) { 240 var ( 241 fqdn string 242 host string 243 err error 244 ) 245 246 host, err = os.Hostname() 247 if err != nil { 248 err = errHostnameFailed{err} 249 goto out 250 } 251 debug("Hostname: %q", host) 252 253 fqdn, err = fromHosts(host) 254 if err == nil { 255 debug("fqdn fetched from hosts: %q", fqdn) 256 goto out 257 } 258 259 fqdn, err = fromLookup(host) 260 if err == nil { 261 debug("fqdn fetched from lookup: %q", fqdn) 262 goto out 263 } 264 265 debug("fqdn fetch failed: %v", err) 266 out: 267 return fqdn, err 268 }