pax_global_header00006660000000000000000000000064137174615030014521gustar00rootroot0000000000000052 comment=869ae776d5881666b8e89ff9f536176ef1736658 go-fqdn-1.0.0/000077500000000000000000000000001371746150300130525ustar00rootroot00000000000000go-fqdn-1.0.0/.github/000077500000000000000000000000001371746150300144125ustar00rootroot00000000000000go-fqdn-1.0.0/.github/workflows/000077500000000000000000000000001371746150300164475ustar00rootroot00000000000000go-fqdn-1.0.0/.github/workflows/go.yml000066400000000000000000000036721371746150300176070ustar00rootroot00000000000000name: Go on: push: branches: '*' pull_request: branches: '*' jobs: gofmt: name: No suggestions from gofmt runs-on: ubuntu-latest steps: - name: Set up Go uses: actions/setup-go@v1 with: go-version: '1.15' - name: Checkout code uses: actions/checkout@v2 - name: gofmt run: | gofmt_out=/tmp/gofmt.out git ls-files -cmoz --exclude-standard '*.go' \ | sort -z \ | xargs -0 gofmt -l -s -d >"$gofmt_out" if [ "$(wc -c "$gofmt_out" | awk '{print $1}')" -ne 0 ]; then cat >&2 <"$gofmt_out" exit 1 fi golangci-lint: name: No errors from golangci-lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Run golangci-lint uses: actions-contrib/golangci-lint@v1 build_test: name: Build and test runs-on: ${{ matrix.os }} strategy: matrix: os: - macos-latest - ubuntu-latest - windows-latest go: - '1.14' - '1.15' steps: - name: Set up Go uses: actions/setup-go@v1 with: go-version: ${{ matrix.go }} - name: Checkout code uses: actions/checkout@v2 - name: Build run: go build -v ./... - name: Test go env: CGO_ENABLED: '0' run: go test -v -tags DEBUG ./... # Resolving on macos does not work as expected when using pure-go # resolver. In particular, net.LookupHost(os.Hostname()) fails with # `no such host'. See readme for more details on this defect. if: matrix.os != 'macos-latest' - name: Test cgo env: CGO_ENABLED: '1' run: go test -v -tags DEBUG ./... - name: Test cgo race env: CGO_ENABLED: '1' run: go test -v -tags DEBUG -race ./... go-fqdn-1.0.0/.golangci.yml000066400000000000000000000007201371746150300154350ustar00rootroot00000000000000run: modules-download-mode: readonly linters-settings: errcheck: check-type-assertions: true govet: enable-all: true linters: enable: - dogsled - gochecknoglobals - gochecknoinits - goconst - gomnd - goprintffuncname - maligned - nakedret - scopelint - unconvert - unparam issues: exclude-rules: - path: _test.go$ linters: - gomnd max-issues-per-linter: 0 max-same-issues: 0 go-fqdn-1.0.0/LICENSE000066400000000000000000000010611371746150300140550ustar00rootroot00000000000000Copyright since 2015 Showmax s.r.o. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. go-fqdn-1.0.0/README.md000066400000000000000000000020451371746150300143320ustar00rootroot00000000000000# go-fqdn Go package to provide reasonable robust access to fully qualified hostname. It first tries to looks up your hostname in hosts file. If that fails, it falls back to doing lookup via dns. Basically it tries to mirror how standard linux `hostname -f` works. For that reason, your hosts file should be configured properly, please refer to hosts(5) for that. It also has no 3rd party dependencies. ## Usage This package uses go modules, so just writing code that uses it should be enough. For example of usage you can check out [the example](examples/example_test.go). Documentation can be found [here](https://pkg.go.dev/github.com/Showmax/go-fqdn?tab=doc). ## Supported go versions Current and current - 1 versions of go are supported. ## Known issues On macos, when **not** using cgo (`CGO_ENABLED=0`), getting the fqdn hostname might not work. Depends on rest of your setup and how `/etc/resolv.conf` looks like. Since that file is not used much (at least based on documentation) by macos programs, it is possible it is not in correct state. go-fqdn-1.0.0/errors.go000066400000000000000000000016561371746150300147250ustar00rootroot00000000000000package fqdn import "fmt" // Error for cases when os.Hostname() fails. var ErrHostnameFailed = errHostnameFailed{} // Error for cases when we could not found fqdn for whatever reason. var ErrFqdnNotFound = errFqdnNotFound{} type errHostnameFailed struct { cause error } func (e errHostnameFailed) Error() string { return fmt.Sprintf("could not get hostname: %v", e.cause) } func (e errHostnameFailed) Unwrap() error { return e.cause } func (e errHostnameFailed) Is(target error) bool { switch target.(type) { case errHostnameFailed: return true default: return false } } type errFqdnNotFound struct { cause error } func (e errFqdnNotFound) Error() string { return fmt.Sprintf("fqdn hostname not found: %v", e.cause) } func (e errFqdnNotFound) Unwrap() error { return e.cause } func (e errFqdnNotFound) Is(target error) bool { switch target.(type) { case errFqdnNotFound: return true default: return false } } go-fqdn-1.0.0/examples/000077500000000000000000000000001371746150300146705ustar00rootroot00000000000000go-fqdn-1.0.0/examples/example_test.go000066400000000000000000000002761371746150300177160ustar00rootroot00000000000000package fqdn_examples import ( "fmt" "github.com/Showmax/go-fqdn" ) func ExampleFqdnHostname() { fqdn, err := fqdn.FqdnHostname() if err != nil { panic(err) } fmt.Println(fqdn) } go-fqdn-1.0.0/fqdn.go000066400000000000000000000122421371746150300143320ustar00rootroot00000000000000package 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 } go-fqdn-1.0.0/fqdn_posix.go000066400000000000000000000001311371746150300155460ustar00rootroot00000000000000// +build !windows package fqdn var hostsPath = "/etc/hosts" //nolint:gochecknoglobals go-fqdn-1.0.0/fqdn_test.go000066400000000000000000000120761371746150300153760ustar00rootroot00000000000000package fqdn import ( "errors" "io/ioutil" "net" "os" "os/exec" "strings" "testing" ) // This package is hard to reasonably test in isolation, so take a shortcut and // assume that no one will set their hostname to localhost. func TestFqdnHostname(t *testing.T) { fqdnHost, err := FqdnHostname() if err != nil { t.Fatalf("Could not fqdn hostname: %v", err) } if fqdnHost == "localhost" { t.Fatalf("Unexpected fqdn, got: %s", fqdnHost) } if net.ParseIP(fqdnHost) != nil { t.Fatalf("Got IP address: %s", fqdnHost) } } func TestFromLookup(t *testing.T) { testCases := []struct { host string err error fqdn string }{ // I mean, these 2 are probably the most static IPs I can get {"ipv4.google.com", nil, "ipv4.l.google.com"}, {"ipv6.google.com", nil, "ipv6.l.google.com"}, {"makwjefalurgaf8", ErrFqdnNotFound, ""}, } for _, tc := range testCases { fqdn, err := fromLookup(tc.host) if !errors.Is(err, tc.err) { t.Fatalf("Unexpected error.\n"+ "\tExpected: %T\n"+ "\tActual : %T\n", tc.err, err) } if fqdn != tc.fqdn { t.Fatalf("Fqdn does not match.\n"+ "\tExpected: %q\n"+ "\tActual : %q\n", tc.fqdn, fqdn) } } } func cat(file string) { content, err := exec.Command("cat", file).Output() if err != nil { // This probably means we are on windows debug("Could not cat %q: %v", file, err) return } debug("%s:\n", file) debug("------------------\n") debug("%q\n", content) debug("------------------\n") debug("%s\n", content) debug("------------------\n") } // In order to behave in expected way, we should verify that we are producing // same output has hostname utility. func TestMatchHostname(t *testing.T) { cat("/etc/hosts") cat("/etc/resolv.conf") out, err := exec.Command(hostnameBin, hostnameArgs...).Output() if err != nil { t.Fatalf("Could not run hostname: %v", err) } outS := chomp(string(out)) fqdn, err := FqdnHostname() if err != nil { t.Fatalf("Could not fqdn hostname: %v", err) } // Since hostnames (domains) are case-insensitive and mac's hostname // returns it with uppercased first letter causing test to fail // // Us : "mac-1271.local" // Them: "Mac-1271.local" // // we should compare lower-cased versions. outS = strings.ToLower(outS) fqdn = strings.ToLower(fqdn) if outS != fqdn { t.Fatalf("Output from hostname does not match.\n"+ "\tUs : %q\n"+ "\tThem: %q\n", fqdn, outS) } } func TestParseHosts(t *testing.T) { testCases := []struct { hosts string host string fqdn string err error }{ { `# Static table lookup for hostnames. # See hosts(5) for details. 127.0.0.1 foo`, "foo", "foo", nil, }, { `# Static table lookup for hostnames. # See hosts(5) for details. 127.0.0.1 bar.foo foo`, "foo", "bar.foo", nil, }, { `# Static table lookup for hostnames. # See hosts(5) for details. 127.0.0.1 yy bar 127.0.0.1 bar.foo foo 127.0.0.1 xx bar`, "foo", "bar.foo", nil, }, { // This one is interesting, since it hostname -f with // this /etc/hosts gives you different results on musl-c // and glibc. I've picked the glibc behaviour, since we // can stop on first match. `# Static table lookup for hostnames. # See hosts(5) for details. 127.0.0.1 bar.foo foo 127.0.0.1 foo.bar foo`, "foo", "bar.foo", nil, }, } for _, tc := range testCases { hosts, err := ioutil.TempFile("", "go-fqdn.hosts.") if err != nil { panic(err) } defer os.Remove(hosts.Name()) if _, err = hosts.Write([]byte(tc.hosts)); err != nil { panic(err) } hostsPath = hosts.Name() fqdn, err := fromHosts(tc.host) if !errors.Is(err, tc.err) { t.Fatalf("Unexpected error.\n"+ "\tExpected: %T\n"+ "\tActual : %T\n", tc.err, err) } if fqdn != tc.fqdn { t.Fatalf("Fqdn does not match.\n"+ "\tExpected: %q\n"+ "\tActual : %q\n", tc.fqdn, fqdn) } } } func TestParseHostLine(t *testing.T) { testCases := []struct { host string line string fqdn string ok bool }{ {"foo", "::1 foo bar", "foo", true}, {"foo", "127.0.0.1 foo bar", "foo", true}, {"bar", "::1 foo bar", "foo", true}, {"bar", "::1 \t foo \t\t\t bar \t\t", "foo", true}, {"bar", "127.0.0.1 foo bar", "foo", true}, {"bar", "127.0.0.1 foo.full bar", "foo.full", true}, {"foo", "::1 foo", "foo", true}, {"foo", "::1 bar", "", false}, {"::1", "::1", "", false}, {"127.0.0.1", "127.0.0.1", "", false}, {"bar", "127.0.0.1 foo # bar", "", false}, {"bar", "127.0.0.1 foo#bar", "", false}, {"bar", "127.0.0.1\tfoo#bar asdawdf a#", "", false}, {"b", "127.0.0.1 a b", "a", true}, {"a", "127.0.0.1 a b", "a", true}, {"c", "127.0.0.1 a b", "", false}, {"b", "127.0.0.1 _invalid_ b", "", false}, {"b", "127.0.0.1 今日は b", "今日は", true}, } for _, tc := range testCases { fqdn, ok := parseHostLine(tc.host, tc.line) if ok != tc.ok { t.Fatalf("Wrong ok value.\n"+ "\tExpected: %t\n"+ "\tActual : %t\n", tc.ok, ok) } if fqdn != tc.fqdn { t.Fatalf("Wrong fqdn value.\n"+ "\tExpected: %q\n"+ "\tActual : %q\n", tc.fqdn, fqdn) } } } go-fqdn-1.0.0/fqdn_test_posix.go000066400000000000000000000001761371746150300166160ustar00rootroot00000000000000// +build !windows package fqdn const hostnameBin = "hostname" var hostnameArgs = []string{"-f"} //nolint:gochecknoglobals go-fqdn-1.0.0/fqdn_test_win.go000066400000000000000000000001711371746150300162440ustar00rootroot00000000000000// +build windows package fqdn const hostnameBin = "hostname" var hostnameArgs = []string{} //nolint:gochecknoglobals go-fqdn-1.0.0/fqdn_win.go000066400000000000000000000001631371746150300152060ustar00rootroot00000000000000// +build windows package fqdn var hostsPath = `C:\Windows\System32\drivers\etc\hosts` //nolint:gochecknoglobals go-fqdn-1.0.0/go.mod000066400000000000000000000000531371746150300141560ustar00rootroot00000000000000module github.com/Showmax/go-fqdn go 1.15 go-fqdn-1.0.0/legacy.go000066400000000000000000000015651371746150300146540ustar00rootroot00000000000000package fqdn import ( "net" "os" "strings" ) // Get Fully Qualified Domain Name // returns "unknown" or hostname in case of error // // Deprecated: // This function has bad API, works poorly and is replace by // FqdnHostname. Please please do not use it. It *will* be removed // in the next version. func Get() string { hostname, err := os.Hostname() if err != nil { return "unknown" } addrs, err := net.LookupIP(hostname) if err != nil { return hostname } for _, addr := range addrs { if ipv4 := addr.To4(); ipv4 != nil { ip, err := ipv4.MarshalText() if err != nil { return hostname } hosts, err := net.LookupAddr(string(ip)) if err != nil || len(hosts) == 0 { return hostname } fqdn := hosts[0] return strings.TrimSuffix(fqdn, ".") // return fqdn without trailing dot } } return hostname } go-fqdn-1.0.0/log.go000066400000000000000000000004121371746150300141570ustar00rootroot00000000000000// +build !DEBUG package fqdn // Internal debug functions which by default does nothing. This allows compiler // to optimize it out so it has no performance impact. If you want the output, // recompile with `-tags DEBUG`. func debug(s string, v ...interface{}) {} go-fqdn-1.0.0/log_debug.go000066400000000000000000000002361371746150300153310ustar00rootroot00000000000000// +build DEBUG package fqdn import "fmt" func debug(s string, v ...interface{}) { if s[len(s)-1] != '\n' { s += string('\n') } fmt.Printf(s, v...) } go-fqdn-1.0.0/util.go000066400000000000000000000004571371746150300143640ustar00rootroot00000000000000package fqdn import ( "bufio" "io" ) // Read lines from r. It strips the line terminators and handles case when last // line is not terminated. func readline(r *bufio.Reader) (string, error) { s, e := r.ReadString('\n') if e == io.EOF && len(s) != 0 { e = nil } s = chomp(s) return s, e } go-fqdn-1.0.0/util_posix.go000066400000000000000000000002131371746150300155740ustar00rootroot00000000000000// +build !windows package fqdn func chomp(s string) string { if len(s) > 0 && s[len(s)-1] == '\n' { s = s[:len(s)-1] } return s } go-fqdn-1.0.0/util_test.go000066400000000000000000000021271371746150300154170ustar00rootroot00000000000000package fqdn import ( "bufio" "io" "strings" "testing" ) type readlineTestCase struct { in string out []string } func testReadline(t *testing.T, testCases []readlineTestCase) { for _, tc := range testCases { var e error var l string debug("Testing with: %q\n", tc.in) r := bufio.NewReader(strings.NewReader(tc.in)) i := 0 for l, e = readline(r); e == nil; l, e = readline(r) { if i >= len(tc.out) { t.Fatalf("Too many lines received") } if tc.out[i] != l { t.Fatalf("Line does not match.\n"+ "\tExpected: %q\n"+ "\tActual : %q\n", tc.out[i], l) } i += 1 } if e != io.EOF { t.Fatalf("Expected EOF, but exception is %T.", e) } if i != len(tc.out) { t.Fatalf("Not enough lines received") } } } func TestReadline(t *testing.T) { testCases := []readlineTestCase{ {"foo\nbar\nbaz\n", []string{"foo", "bar", "baz"}}, {"foo\nbar\nbaz", []string{"foo", "bar", "baz"}}, {"foo\nbar\nbaz\n\n", []string{"foo", "bar", "baz", ""}}, {"foo\nbar\nbaz\n\nx", []string{"foo", "bar", "baz", "", "x"}}, } testReadline(t, testCases) } go-fqdn-1.0.0/util_win.go000066400000000000000000000003111371746150300152260ustar00rootroot00000000000000// +build windows package fqdn func chomp(s string) string { if len(s) > 0 && s[len(s)-1] == '\n' { s = s[:len(s)-1] } if len(s) > 0 && s[len(s)-1] == '\r' { s = s[:len(s)-1] } return s } go-fqdn-1.0.0/util_win_test.go000066400000000000000000000007151371746150300162750ustar00rootroot00000000000000// +build windows package fqdn import ( "testing" ) func TestReadlineWin(t *testing.T) { testCases := []readlineTestCase{ {"foo\r\nbar\r\nbaz\r\n", []string{"foo", "bar", "baz"}}, {"foo\r\nbar\r\nbaz", []string{"foo", "bar", "baz"}}, {"foo\r\nbar\r\nbz\r\n\r\n", []string{"foo", "bar", "bz", ""}}, {"foo\nbar\rbz\n\r", []string{"foo", "bar\rbz", ""}}, {"foo\nbar\r\nbz\r\n\n", []string{"foo", "bar", "bz", ""}}, } testReadline(t, testCases) }