Added install methods to openapi.yaml

Print all net interfaces when bind_host is 0.0.0.0
This commit is contained in:
Andrey Meshkov 2019-02-22 17:59:42 +03:00
parent e8898811fe
commit 4e1c1618cb
6 changed files with 224 additions and 56 deletions

33
app.go
View File

@ -208,8 +208,7 @@ func run(args options) {
}, },
} }
URL := fmt.Sprintf("https://%s", address) printHTTPAddresses("https")
log.Println("Go to " + URL)
err = httpsServer.server.ListenAndServeTLS("", "") err = httpsServer.server.ListenAndServeTLS("", "")
if err != http.ErrServerClosed { if err != http.ErrServerClosed {
log.Fatal(err) 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 // this loop is used as an ability to change listening host and/or port
for { for {
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort)) printHTTPAddresses("http")
URL := fmt.Sprintf("http://%s", address)
log.Println("Go to " + URL)
// we need to have new instance, because after Shutdown() the Server is not usable // 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{ httpServer = &http.Server{
Addr: address, Addr: address,
} }
@ -395,3 +394,27 @@ func loadOptions() options {
return o 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)
}
}

View File

@ -32,11 +32,11 @@ type configuration struct {
ourWorkingDir string // Location of our directory, used to protect against CWD being somewhere else 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 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"` BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
BindPort int `yaml:"bind_port"` BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
AuthName string `yaml:"auth_name"` AuthName string `yaml:"auth_name"` // AuthName is the basic auth username
AuthPass string `yaml:"auth_pass"` AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password
Language string `yaml:"language"` // two-letter ISO 639-1 language code Language string `yaml:"language"` // two-letter ISO 639-1 language code
DNS dnsConfig `yaml:"dns"` DNS dnsConfig `yaml:"dns"`
TLS tlsConfig `yaml:"tls"` TLS tlsConfig `yaml:"tls"`
Filters []filter `yaml:"filters"` Filters []filter `yaml:"filters"`

View File

@ -909,17 +909,6 @@ type firstRunData struct {
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data := firstRunData{} 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 // find out if port 80 is available -- if not, fall back to 3000
if checkPortAvailable("", 80) == nil { 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." 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{}) data.Interfaces = make(map[string]interface{})
for _, iface := range ifaces { for _, iface := range ifaces {
addrs, e := iface.Addrs() data.Interfaces[iface.Name] = iface
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
}
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -983,7 +946,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
newSettings := firstRunData{} newSettings := firstRunData{}
err := json.NewDecoder(r.Body).Decode(&newSettings) err := json.NewDecoder(r.Body).Decode(&newSettings)
if err != nil { 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 return
} }

View File

@ -15,6 +15,8 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"github.com/joomcode/errorx"
) )
// ---------------------------------- // ----------------------------------
@ -237,6 +239,56 @@ func getValidNetInterfaces() ([]net.Interface, error) {
return netIfaces, nil 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 // 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 { func checkPortAvailable(host string, port int) error {
ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port))) ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port)))

25
helpers_test.go Normal file
View File

@ -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)
}
}

View File

@ -39,6 +39,9 @@ tags:
- -
name: dhcp name: dhcp
description: 'Built-in DHCP server controls' description: 'Built-in DHCP server controls'
-
name: install
description: 'First-time install configuration handlers'
paths: paths:
# API TO-DO LIST # API TO-DO LIST
@ -713,6 +716,42 @@ paths:
text/plain: text/plain:
en 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: definitions:
ServerStatus: ServerStatus:
type: "object" type: "object"
@ -1207,4 +1246,70 @@ definitions:
warning_validation: warning_validation:
type: "string" type: "string"
example: "You have specified an empty certificate" example: "You have specified an empty certificate"
description: "warning_validation is a validation warning message with the issue description" 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"