From 4e1c1618cb62153510fb0c538438e9e39c979923 Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Fri, 22 Feb 2019 17:59:42 +0300 Subject: [PATCH] Added install methods to openapi.yaml Print all net interfaces when bind_host is 0.0.0.0 --- app.go | 33 +++++++++++-- config.go | 10 ++-- control.go | 53 ++++----------------- helpers.go | 52 +++++++++++++++++++++ helpers_test.go | 25 ++++++++++ openapi/openapi.yaml | 107 ++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 224 insertions(+), 56 deletions(-) create mode 100644 helpers_test.go diff --git a/app.go b/app.go index 1abad8c5..50d378d1 100644 --- a/app.go +++ b/app.go @@ -208,8 +208,7 @@ func run(args options) { }, } - URL := fmt.Sprintf("https://%s", address) - log.Println("Go to " + URL) + printHTTPAddresses("https") err = httpsServer.server.ListenAndServeTLS("", "") if err != http.ErrServerClosed { log.Fatal(err) @@ -220,10 +219,10 @@ func run(args options) { // this loop is used as an ability to change listening host and/or port for { - address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort)) - URL := fmt.Sprintf("http://%s", address) - log.Println("Go to " + URL) + printHTTPAddresses("http") + // we need to have new instance, because after Shutdown() the Server is not usable + address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort)) httpServer = &http.Server{ Addr: address, } @@ -395,3 +394,27 @@ func loadOptions() options { return o } + +// prints IP addresses which user can use to open the admin interface +// proto is either "http" or "https" +func printHTTPAddresses(proto string) { + var address string + if config.BindHost == "0.0.0.0" { + log.Println("AdGuard Home is available on the following addresses:") + ifaces, err := getValidNetInterfacesForWeb() + if err != nil { + // That's weird, but we'll ignore it + address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort)) + log.Printf("Go to %s://%s", proto, address) + return + } + + for _, iface := range ifaces { + address = net.JoinHostPort(iface.Addresses[0], strconv.Itoa(config.BindPort)) + log.Printf("Go to %s://%s", proto, address) + } + } else { + address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort)) + log.Printf("Go to %s://%s", proto, address) + } +} diff --git a/config.go b/config.go index 158381c1..87e5c6a8 100644 --- a/config.go +++ b/config.go @@ -32,11 +32,11 @@ type configuration struct { ourWorkingDir string // Location of our directory, used to protect against CWD being somewhere else firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html - BindHost string `yaml:"bind_host"` - BindPort int `yaml:"bind_port"` - AuthName string `yaml:"auth_name"` - AuthPass string `yaml:"auth_pass"` - Language string `yaml:"language"` // two-letter ISO 639-1 language code + BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to + BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server + AuthName string `yaml:"auth_name"` // AuthName is the basic auth username + AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password + Language string `yaml:"language"` // two-letter ISO 639-1 language code DNS dnsConfig `yaml:"dns"` TLS tlsConfig `yaml:"tls"` Filters []filter `yaml:"filters"` diff --git a/control.go b/control.go index e6431941..596e054d 100644 --- a/control.go +++ b/control.go @@ -909,17 +909,6 @@ type firstRunData struct { func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { data := firstRunData{} - ifaces, err := getValidNetInterfaces() - if err != nil { - httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err) - return - } - if len(ifaces) == 0 { - httpError(w, http.StatusServiceUnavailable, "Couldn't find any legible interface, plase try again later") - return - } - - // fill out the fields // find out if port 80 is available -- if not, fall back to 3000 if checkPortAvailable("", 80) == nil { @@ -934,41 +923,15 @@ func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { data.DNS.Warning = "Port 53 is not available for binding -- this will make DNS clients unable to contact AdGuard Home." } + ifaces, err := getValidNetInterfacesForWeb() + if err != nil { + httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err) + return + } + data.Interfaces = make(map[string]interface{}) for _, iface := range ifaces { - addrs, e := iface.Addrs() - if e != nil { - httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %s: %s", iface.Name, err) - return - } - - jsonIface := netInterface{ - Name: iface.Name, - MTU: iface.MTU, - HardwareAddr: iface.HardwareAddr.String(), - } - - if iface.Flags != 0 { - jsonIface.Flags = iface.Flags.String() - } - - // we don't want link-local addresses in json, so skip them - for _, addr := range addrs { - ipnet, ok := addr.(*net.IPNet) - if !ok { - // not an IPNet, should not happen - httpError(w, http.StatusInternalServerError, "SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr) - return - } - // ignore link-local - if ipnet.IP.IsLinkLocalUnicast() { - continue - } - jsonIface.Addresses = append(jsonIface.Addresses, ipnet.IP.String()) - } - if len(jsonIface.Addresses) != 0 { - data.Interfaces[iface.Name] = jsonIface - } + data.Interfaces[iface.Name] = iface } w.Header().Set("Content-Type", "application/json") @@ -983,7 +946,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) { newSettings := firstRunData{} err := json.NewDecoder(r.Body).Decode(&newSettings) if err != nil { - httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err) + httpError(w, http.StatusBadRequest, "Failed to parse new config json: %s", err) return } diff --git a/helpers.go b/helpers.go index 822c77df..8e884d36 100644 --- a/helpers.go +++ b/helpers.go @@ -15,6 +15,8 @@ import ( "runtime" "strconv" "strings" + + "github.com/joomcode/errorx" ) // ---------------------------------- @@ -237,6 +239,56 @@ func getValidNetInterfaces() ([]net.Interface, error) { return netIfaces, nil } +// getValidNetInterfacesMap returns interfaces that are eligible for DNS and WEB only +// we do not return link-local addresses here +func getValidNetInterfacesForWeb() ([]netInterface, error) { + ifaces, err := getValidNetInterfaces() + if err != nil { + return nil, errorx.Decorate(err, "Couldn't get interfaces") + } + if len(ifaces) == 0 { + return nil, errors.New("couldn't find any legible interface") + } + + var netInterfaces []netInterface + + for _, iface := range ifaces { + addrs, e := iface.Addrs() + if e != nil { + return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name) + } + + netIface := netInterface{ + Name: iface.Name, + MTU: iface.MTU, + HardwareAddr: iface.HardwareAddr.String(), + } + + if iface.Flags != 0 { + netIface.Flags = iface.Flags.String() + } + + // we don't want link-local addresses in json, so skip them + for _, addr := range addrs { + ipnet, ok := addr.(*net.IPNet) + if !ok { + // not an IPNet, should not happen + return nil, fmt.Errorf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr) + } + // ignore link-local + if ipnet.IP.IsLinkLocalUnicast() { + continue + } + netIface.Addresses = append(netIface.Addresses, ipnet.IP.String()) + } + if len(netIface.Addresses) != 0 { + netInterfaces = append(netInterfaces, netIface) + } + } + + return netInterfaces, nil +} + // checkPortAvailable is not a cheap test to see if the port is bindable, because it's actually doing the bind momentarily func checkPortAvailable(host string, port int) error { ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port))) diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 00000000..66fc3d27 --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,25 @@ +package main + +import ( + "testing" + + "github.com/hmage/golibs/log" +) + +func TestGetValidNetInterfacesForWeb(t *testing.T) { + ifaces, err := getValidNetInterfacesForWeb() + if err != nil { + t.Fatalf("Cannot get net interfaces: %s", err) + } + if len(ifaces) == 0 { + t.Fatalf("No net interfaces found") + } + + for _, iface := range ifaces { + if len(iface.Addresses) == 0 { + t.Fatalf("No addresses found for %s", iface.Name) + } + + log.Printf("%v", iface) + } +} diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 452e5036..f1e23f86 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -39,6 +39,9 @@ tags: - name: dhcp description: 'Built-in DHCP server controls' + - + name: install + description: 'First-time install configuration handlers' paths: # API TO-DO LIST @@ -713,6 +716,42 @@ paths: text/plain: en + # -------------------------------------------------- + # First-time install configuration methods + # -------------------------------------------------- + + /install/get_addresses: + get: + tags: + - install + operationId: installGetAddresses + summary: "Gets the network interfaces information." + responses: + 200: + description: OK + schema: + $ref: "#/definitions/AddressesInfo" + /install/configure: + post: + tags: + - install + operationId: installConfigure + summary: "Applies the initial configuration." + parameters: + - in: "body" + name: "body" + description: "Initial configuration JSON" + required: true + schema: + $ref: "#/definitions/InitialConfiguration" + responses: + 200: + description: OK + 400: + description: "Failed to parse initial configuration or cannot listen to the specified addresses" + 500: + description: "Cannot start the DNS server" + definitions: ServerStatus: type: "object" @@ -1207,4 +1246,70 @@ definitions: warning_validation: type: "string" example: "You have specified an empty certificate" - description: "warning_validation is a validation warning message with the issue description" \ No newline at end of file + description: "warning_validation is a validation warning message with the issue description" + NetInterface: + type: "object" + description: "Network interface info" + properties: + flags: + type: "string" + example: "up|broadcast|multicast" + hardware_address: + type: "string" + example: "52:54:00:11:09:ba" + mtu: + type: "integer" + format: "int32" + example: 1500 + name: + type: "string" + example: "eth0" + ip_addresses: + type: "array" + items: + type: "string" + example: + - "127.0.0.1" + AddressInfo: + type: "object" + description: "Port information" + properties: + ip: + type: "string" + example: "127.0.01" + port: + type: "integer" + format: "int32" + example: 53 + warning: + type: "string" + example: "Cannot bind to this port" + AddressesInfo: + type: "object" + description: "AdGuard Home addresses configuration" + properties: + dns: + $ref: "#/definitions/AddressInfo" + web: + $ref: "#/definitions/AddressInfo" + interfaces: + type: "object" + description: "Network interfaces dictionary (key is the interface name)" + additionalProperties: + $ref: "#/definitions/NetInterface" + InitialConfiguration: + type: "object" + description: "AdGuard Home initial configuration (for the first-install wizard)" + properties: + dns: + $ref: "#/definitions/AddressInfo" + web: + $ref: "#/definitions/AddressInfo" + username: + type: "string" + description: "Basic auth username" + example: "admin" + password: + type: "string" + description: "Basic auth password" + example: "password" \ No newline at end of file