added full URL for listening server
This commit is contained in:
@ -5,7 +5,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"supervisor/internal/config"
|
"supervisor/internal/config"
|
||||||
@ -56,7 +58,7 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
func (a *App) Run(ctx context.Context) error {
|
func (a *App) Run(ctx context.Context) error {
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
a.logger.Printf("HTTP server listening on %s", a.cfg.Addr)
|
a.logServerURLs()
|
||||||
errCh <- a.httpServer.ListenAndServe()
|
errCh <- a.httpServer.ListenAndServe()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -73,3 +75,134 @@ func (a *App) Run(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) logServerURLs() {
|
||||||
|
urls, err := serverURLs(a.cfg.Addr)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Printf("warning: determine local server URLs: %v", err)
|
||||||
|
}
|
||||||
|
if len(urls) == 0 {
|
||||||
|
a.logger.Printf("HTTP server listening on %s", a.cfg.Addr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Print("HTTP server available at:")
|
||||||
|
for _, url := range urls {
|
||||||
|
a.logger.Printf(" %s", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverURLs(addr string) ([]string, error) {
|
||||||
|
return serverURLsWithLocalIPv4Addrs(addr, discoverLocalIPv4Addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverURLsWithLocalIPv4Addrs(addr string, localIPv4Addrs func() ([]string, error)) ([]string, error) {
|
||||||
|
host, port, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
urls := make([]string, 0, 4)
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
addURL := func(host string) {
|
||||||
|
if host == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
url := "http://" + net.JoinHostPort(host, port)
|
||||||
|
if _, ok := seen[url]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[url] = struct{}{}
|
||||||
|
urls = append(urls, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isWildcardHost(host) {
|
||||||
|
addURL("localhost")
|
||||||
|
addURL("127.0.0.1")
|
||||||
|
|
||||||
|
addrs, err := localIPv4Addrs()
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ip := net.ParseIP(addr)
|
||||||
|
if !isUsableLocalIPv4(ip) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addURL(ip.String())
|
||||||
|
}
|
||||||
|
return urls, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
addURL(host)
|
||||||
|
return urls, nil
|
||||||
|
}
|
||||||
|
if ip4 := ip.To4(); ip4 != nil && !ip4.IsUnspecified() {
|
||||||
|
addURL(ip4.String())
|
||||||
|
}
|
||||||
|
return urls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWildcardHost(host string) bool {
|
||||||
|
return host == "" || host == "0.0.0.0" || host == "::"
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverLocalIPv4Addrs() ([]string, error) {
|
||||||
|
interfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs := make([]string, 0)
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ifaceAddrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return addrs, err
|
||||||
|
}
|
||||||
|
for _, ifaceAddr := range ifaceAddrs {
|
||||||
|
ip := ipFromAddr(ifaceAddr)
|
||||||
|
if !isUsableLocalIPv4(ip) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := ip.To4().String()
|
||||||
|
if _, ok := seen[addr]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[addr] = struct{}{}
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(addrs)
|
||||||
|
return addrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipFromAddr(addr net.Addr) net.IP {
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
return v.IP
|
||||||
|
case *net.IPAddr:
|
||||||
|
return v.IP
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUsableLocalIPv4(ip net.IP) bool {
|
||||||
|
if ip == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ip4 := ip.To4()
|
||||||
|
if ip4 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !ip4.IsUnspecified() &&
|
||||||
|
!ip4.IsLoopback() &&
|
||||||
|
!ip4.IsMulticast() &&
|
||||||
|
!ip4.IsLinkLocalUnicast()
|
||||||
|
}
|
||||||
|
|||||||
122
internal/app/app_test.go
Normal file
122
internal/app/app_test.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServerURLsForWildcardAddr(t *testing.T) {
|
||||||
|
urls, err := serverURLsWithLocalIPv4Addrs(":8080", func() ([]string, error) {
|
||||||
|
return []string{
|
||||||
|
"192.168.1.100",
|
||||||
|
"127.0.0.1",
|
||||||
|
"192.168.1.100",
|
||||||
|
"10.0.0.2",
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("serverURLsWithLocalIPv4Addrs returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{
|
||||||
|
"http://localhost:8080",
|
||||||
|
"http://127.0.0.1:8080",
|
||||||
|
"http://192.168.1.100:8080",
|
||||||
|
"http://10.0.0.2:8080",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(urls, want) {
|
||||||
|
t.Fatalf("urls = %#v, want %#v", urls, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerURLsForZeroIPv4Addr(t *testing.T) {
|
||||||
|
urls, err := serverURLsWithLocalIPv4Addrs("0.0.0.0:8080", func() ([]string, error) {
|
||||||
|
return []string{"192.168.1.100"}, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("serverURLsWithLocalIPv4Addrs returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{
|
||||||
|
"http://localhost:8080",
|
||||||
|
"http://127.0.0.1:8080",
|
||||||
|
"http://192.168.1.100:8080",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(urls, want) {
|
||||||
|
t.Fatalf("urls = %#v, want %#v", urls, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerURLsForSpecificIPv4Addr(t *testing.T) {
|
||||||
|
urls, err := serverURLsWithLocalIPv4Addrs("192.168.1.100:8080", func() ([]string, error) {
|
||||||
|
t.Fatal("local IPv4 addrs should not be discovered for a specific bind address")
|
||||||
|
return nil, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("serverURLsWithLocalIPv4Addrs returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{"http://192.168.1.100:8080"}
|
||||||
|
if !reflect.DeepEqual(urls, want) {
|
||||||
|
t.Fatalf("urls = %#v, want %#v", urls, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerURLsForLocalhostAddr(t *testing.T) {
|
||||||
|
urls, err := serverURLsWithLocalIPv4Addrs("localhost:8080", func() ([]string, error) {
|
||||||
|
t.Fatal("local IPv4 addrs should not be discovered for a hostname bind address")
|
||||||
|
return nil, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("serverURLsWithLocalIPv4Addrs returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{"http://localhost:8080"}
|
||||||
|
if !reflect.DeepEqual(urls, want) {
|
||||||
|
t.Fatalf("urls = %#v, want %#v", urls, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerURLsReturnsFallbackURLsWhenDiscoveryFails(t *testing.T) {
|
||||||
|
wantErr := errors.New("interfaces unavailable")
|
||||||
|
urls, err := serverURLsWithLocalIPv4Addrs(":8080", func() ([]string, error) {
|
||||||
|
return nil, wantErr
|
||||||
|
})
|
||||||
|
if !errors.Is(err, wantErr) {
|
||||||
|
t.Fatalf("serverURLsWithLocalIPv4Addrs error = %v, want %v", err, wantErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{
|
||||||
|
"http://localhost:8080",
|
||||||
|
"http://127.0.0.1:8080",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(urls, want) {
|
||||||
|
t.Fatalf("urls = %#v, want %#v", urls, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsUsableLocalIPv4(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ip net.IP
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "private", ip: net.ParseIP("192.168.1.100"), want: true},
|
||||||
|
{name: "zero", ip: net.ParseIP("0.0.0.0"), want: false},
|
||||||
|
{name: "loopback", ip: net.ParseIP("127.0.0.1"), want: false},
|
||||||
|
{name: "ipv6", ip: net.ParseIP("2001:db8::1"), want: false},
|
||||||
|
{name: "multicast", ip: net.ParseIP("224.0.0.1"), want: false},
|
||||||
|
{name: "link local", ip: net.ParseIP("169.254.1.2"), want: false},
|
||||||
|
{name: "nil", ip: nil, want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := isUsableLocalIPv4(tt.ip); got != tt.want {
|
||||||
|
t.Fatalf("isUsableLocalIPv4(%v) = %v, want %v", tt.ip, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user