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  }