package fqdn
import (
"bufio"
"fmt"
"io"
"net"
"os"
)
// isalnum(3p) in POSIX locale
func isalnum(r rune) bool {
return (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9')
}
const (
maxHostnameLen = 254
)
// Validate hostname, based on musl-c version of this function.
func isValidHostname(s string) bool {
if len(s) > maxHostnameLen {
return false
}
for _, c := range s {
if !(c >= 0x80 || c == '.' || c == '-' || isalnum(c)) {
return false
}
}
return true
}
func parseHostLine(host string, line string) (string, bool) {
const (
StateSkipWhite = iota
StateIp
StateCanonFirst
StateCanon
StateAliasFirst
StateAlias
)
var (
canon string
state int
nextState int
i int
start int
)
isWhite := func(b byte) bool {
return b == ' ' || b == '\t'
}
isLast := func() bool {
return i == len(line)-1 || isWhite(line[i+1])
}
partStr := func() string {
return line[start : i+1]
}
state = StateSkipWhite
nextState = StateIp
debug("Looking for %q in %q", host, line)
for i = 0; i < len(line); i += 1 {
debug("%03d: character %q, state: %d, nstate: %d",
i, line[i], state, nextState)
if line[i] == '#' {
debug("%03d: found comment, terminating", i)
break
}
switch state {
case StateSkipWhite:
if !isWhite(line[i]) {
state = nextState
i -= 1
}
case StateIp:
if isLast() {
state = StateSkipWhite
nextState = StateCanonFirst
}
case StateCanonFirst:
start = i
state = StateCanon
i -= 1
case StateCanon:
debug("Canon so far: %q", partStr())
if isLast() {
canon = partStr()
if !isValidHostname(canon) {
return "", false
}
if canon == host {
debug("Canon match")
return canon, true
}
state = StateSkipWhite
nextState = StateAliasFirst
}
case StateAliasFirst:
start = i
state = StateAlias
i -= 1
case StateAlias:
debug("Alias so far: %q", partStr())
if isLast() {
alias := partStr()
if alias == host {
debug("Alias match")
return canon, true
}
state = StateSkipWhite
nextState = StateAliasFirst
}
default:
panic(fmt.Sprintf("BUG: State not handled: %d", state))
}
}
debug("No match")
return "", false
}
// Reads hosts(5) file and tries to get canonical name for host.
func fromHosts(host string) (string, error) {
var (
fqdn string
line string
err error
file *os.File
r *bufio.Reader
ok bool
)
file, err = os.Open(hostsPath)
if err != nil {
err = fmt.Errorf("cannot open hosts file: %w", err)
goto out
}
defer file.Close()
r = bufio.NewReader(file)
for line, err = readline(r); err == nil; line, err = readline(r) {
fqdn, ok = parseHostLine(host, line)
if ok {
goto out
}
}
if err != io.EOF {
err = fmt.Errorf("failed to read file: %w", err)
goto out
}
err = errFqdnNotFound{}
out:
return fqdn, err
}
func fromLookup(host string) (string, error) {
var (
fqdn string
err error
addrs []net.IP
hosts []string
)
fqdn, err = net.LookupCNAME(host)
if err == nil && len(fqdn) != 0 {
debug("LookupCNAME success: %q", fqdn)
goto out
}
debug("LookupCNAME failed: %v", err)
debug("Looking up: %q", host)
addrs, err = net.LookupIP(host)
if err != nil {
err = errFqdnNotFound{err}
goto out
}
debug("Resolved addrs: %q", addrs)
for _, addr := range addrs {
debug("Trying: %q", addr)
hosts, err = net.LookupAddr(addr.String())
// On windows it can return err == nil but empty list of hosts
if err != nil || len(hosts) == 0 {
continue
}
debug("Resolved hosts: %q", hosts)
// First one should be the canonical hostname
fqdn = hosts[0]
goto out
}
err = errFqdnNotFound{}
out:
// For some reason we wanted the canonical hostname without
// trailing dot. So if it is present, strip it.
if len(fqdn) > 0 && fqdn[len(fqdn)-1] == '.' {
fqdn = fqdn[:len(fqdn)-1]
}
return fqdn, err
}
// Try to get fully qualified hostname for current machine.
//
// It tries to mimic how `hostname -f` works, so except for few edge cases you
// should get the same result from both. One thing that needs to be mentioned is
// that it does not guarantee that you get back fqdn. There is no way to do that
// and `hostname -f` can also return non-fqdn hostname if your /etc/hosts is
// fucked up.
//
// It checks few sources in this order:
//
// 1. hosts file
// It parses hosts file if present and readable and returns first canonical
// hostname that also references your hostname. See hosts(5) for more
// details.
// 2. dns lookup
// If lookup in hosts file fails, it tries to ask dns.
//
// If none of steps above succeeds, ErrFqdnNotFound is returned as error. You
// will probably want to just use output from os.Hostname() at that point.
func FqdnHostname() (string, error) {
var (
fqdn string
host string
err error
)
host, err = os.Hostname()
if err != nil {
err = errHostnameFailed{err}
goto out
}
debug("Hostname: %q", host)
fqdn, err = fromHosts(host)
if err == nil {
debug("fqdn fetched from hosts: %q", fqdn)
goto out
}
fqdn, err = fromLookup(host)
if err == nil {
debug("fqdn fetched from lookup: %q", fqdn)
goto out
}
debug("fqdn fetch failed: %v", err)
out:
return fqdn, err
}