diff --git a/control.go b/control.go index 3dca2c36..35214d82 100644 --- a/control.go +++ b/control.go @@ -2,14 +2,12 @@ package main import ( "bytes" - "context" "encoding/json" "fmt" "io/ioutil" "net" "net/http" "os" - "os/exec" "sort" "strconv" "strings" @@ -971,263 +969,6 @@ func handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) { } } -type firstRunData struct { - WebPort int `json:"web_port"` - DNSPort int `json:"dns_port"` - Interfaces map[string]interface{} `json:"interfaces"` -} - -// Get initial installation settings -func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { - log.Tracef("%s %v", r.Method, r.URL) - data := firstRunData{} - data.WebPort = 80 - data.DNSPort = 53 - - 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 { - data.Interfaces[iface.Name] = iface - } - - w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(data) - if err != nil { - httpError(w, http.StatusInternalServerError, "Unable to marshal default addresses to json: %s", err) - return - } -} - -type checkConfigReqEnt struct { - Port int `json:"port"` - IP string `json:"ip"` - Autofix bool `json:"autofix"` -} -type checkConfigReq struct { - Web checkConfigReqEnt `json:"web"` - DNS checkConfigReqEnt `json:"dns"` -} - -type checkConfigRespEnt struct { - Status string `json:"status"` - CanAutofix bool `json:"can_autofix"` -} -type checkConfigResp struct { - Web checkConfigRespEnt `json:"web"` - DNS checkConfigRespEnt `json:"dns"` -} - -// Check if ports are available, respond with results -func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) { - log.Tracef("%s %v", r.Method, r.URL) - reqData := checkConfigReq{} - respData := checkConfigResp{} - err := json.NewDecoder(r.Body).Decode(&reqData) - if err != nil { - httpError(w, http.StatusBadRequest, "Failed to parse 'check_config' JSON data: %s", err) - return - } - - if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort { - err = checkPortAvailable(reqData.Web.IP, reqData.Web.Port) - if err != nil { - respData.Web.Status = fmt.Sprintf("%v", err) - } - } - - if reqData.DNS.Port != 0 { - err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port) - - if errorIsAddrInUse(err) { - canAutofix := checkDNSStubListener() - if canAutofix && reqData.DNS.Autofix { - - err = disableDNSStubListener() - if err != nil { - log.Error("Couldn't disable DNSStubListener: %s", err) - } - - err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port) - canAutofix = false - } - - respData.DNS.CanAutofix = canAutofix - } - - if err == nil { - err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port) - } - - if err != nil { - respData.DNS.Status = fmt.Sprintf("%v", err) - } - } - - w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(respData) - if err != nil { - httpError(w, http.StatusInternalServerError, "Unable to marshal JSON: %s", err) - return - } -} - -// Check if DNSStubListener is active -func checkDNSStubListener() bool { - cmd := exec.Command("systemctl", "is-enabled", "systemd-resolved") - log.Tracef("executing %s %v", cmd.Path, cmd.Args) - _, err := cmd.Output() - if err != nil || cmd.ProcessState.ExitCode() != 0 { - log.Error("command %s has failed: %v code:%d", - cmd.Path, err, cmd.ProcessState.ExitCode()) - return false - } - - cmd = exec.Command("grep", "-E", "#?DNSStubListener=yes", "/etc/systemd/resolved.conf") - log.Tracef("executing %s %v", cmd.Path, cmd.Args) - _, err = cmd.Output() - if err != nil || cmd.ProcessState.ExitCode() != 0 { - log.Error("command %s has failed: %v code:%d", - cmd.Path, err, cmd.ProcessState.ExitCode()) - return false - } - - return true -} - -// Deactivate DNSStubListener -func disableDNSStubListener() error { - cmd := exec.Command("sed", "-r", "-i.orig", "s/#?DNSStubListener=yes/DNSStubListener=no/g", "/etc/systemd/resolved.conf") - log.Tracef("executing %s %v", cmd.Path, cmd.Args) - _, err := cmd.Output() - if err != nil { - return err - } - if cmd.ProcessState.ExitCode() != 0 { - return fmt.Errorf("process %s exited with an error: %d", - cmd.Path, cmd.ProcessState.ExitCode()) - } - - cmd = exec.Command("systemctl", "reload-or-restart", "systemd-resolved") - log.Tracef("executing %s %v", cmd.Path, cmd.Args) - _, err = cmd.Output() - if err != nil { - return err - } - if cmd.ProcessState.ExitCode() != 0 { - return fmt.Errorf("process %s exited with an error: %d", - cmd.Path, cmd.ProcessState.ExitCode()) - } - - return nil -} - -type applyConfigReqEnt struct { - IP string `json:"ip"` - Port int `json:"port"` -} -type applyConfigReq struct { - Web applyConfigReqEnt `json:"web"` - DNS applyConfigReqEnt `json:"dns"` - Username string `json:"username"` - Password string `json:"password"` -} - -// Copy installation parameters between two configuration objects -func copyInstallSettings(dst *configuration, src *configuration) { - dst.BindHost = src.BindHost - dst.BindPort = src.BindPort - dst.DNS.BindHost = src.DNS.BindHost - dst.DNS.Port = src.DNS.Port - dst.AuthName = src.AuthName - dst.AuthPass = src.AuthPass -} - -// Apply new configuration, start DNS server, restart Web server -func handleInstallConfigure(w http.ResponseWriter, r *http.Request) { - log.Tracef("%s %v", r.Method, r.URL) - newSettings := applyConfigReq{} - err := json.NewDecoder(r.Body).Decode(&newSettings) - if err != nil { - httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err) - return - } - - if newSettings.Web.Port == 0 || newSettings.DNS.Port == 0 { - httpError(w, http.StatusBadRequest, "port value can't be 0") - return - } - - restartHTTP := true - if config.BindHost == newSettings.Web.IP && config.BindPort == newSettings.Web.Port { - // no need to rebind - restartHTTP = false - } - - // validate that hosts and ports are bindable - if restartHTTP { - err = checkPortAvailable(newSettings.Web.IP, newSettings.Web.Port) - if err != nil { - httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s", - net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err) - return - } - } - - err = checkPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) - if err != nil { - httpError(w, http.StatusBadRequest, "%s", err) - return - } - - err = checkPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) - if err != nil { - httpError(w, http.StatusBadRequest, "%s", err) - return - } - - var curConfig configuration - copyInstallSettings(&curConfig, &config) - - config.firstRun = false - config.BindHost = newSettings.Web.IP - config.BindPort = newSettings.Web.Port - config.DNS.BindHost = newSettings.DNS.IP - config.DNS.Port = newSettings.DNS.Port - config.AuthName = newSettings.Username - config.AuthPass = newSettings.Password - - err = startDNSServer() - if err != nil { - config.firstRun = true - copyInstallSettings(&config, &curConfig) - httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err) - return - } - - err = config.write() - if err != nil { - config.firstRun = true - copyInstallSettings(&config, &curConfig) - httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err) - return - } - - // this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block - // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely - if restartHTTP { - go func() { - httpServer.Shutdown(context.TODO()) - }() - } - - returnOK(w) -} - // -------------- // DNS-over-HTTPS // -------------- @@ -1249,12 +990,6 @@ func handleDOH(w http.ResponseWriter, r *http.Request) { // ------------------------ // registration of handlers // ------------------------ -func registerInstallHandlers() { - http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses))) - http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(handleInstallCheckConfig))) - http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure))) -} - func registerControlHandlers() { http.HandleFunc("/control/status", postInstall(optionalAuth(ensureGET(handleStatus)))) http.HandleFunc("/control/enable_protection", postInstall(optionalAuth(ensurePOST(handleProtectionEnable)))) diff --git a/control_install.go b/control_install.go new file mode 100644 index 00000000..c4338e9d --- /dev/null +++ b/control_install.go @@ -0,0 +1,276 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "os/exec" + "strconv" + + "github.com/AdguardTeam/golibs/log" +) + +type firstRunData struct { + WebPort int `json:"web_port"` + DNSPort int `json:"dns_port"` + Interfaces map[string]interface{} `json:"interfaces"` +} + +// Get initial installation settings +func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + data := firstRunData{} + data.WebPort = 80 + data.DNSPort = 53 + + 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 { + data.Interfaces[iface.Name] = iface + } + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(data) + if err != nil { + httpError(w, http.StatusInternalServerError, "Unable to marshal default addresses to json: %s", err) + return + } +} + +type checkConfigReqEnt struct { + Port int `json:"port"` + IP string `json:"ip"` + Autofix bool `json:"autofix"` +} +type checkConfigReq struct { + Web checkConfigReqEnt `json:"web"` + DNS checkConfigReqEnt `json:"dns"` +} + +type checkConfigRespEnt struct { + Status string `json:"status"` + CanAutofix bool `json:"can_autofix"` +} +type checkConfigResp struct { + Web checkConfigRespEnt `json:"web"` + DNS checkConfigRespEnt `json:"dns"` +} + +// Check if ports are available, respond with results +func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + reqData := checkConfigReq{} + respData := checkConfigResp{} + err := json.NewDecoder(r.Body).Decode(&reqData) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to parse 'check_config' JSON data: %s", err) + return + } + + if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort { + err = checkPortAvailable(reqData.Web.IP, reqData.Web.Port) + if err != nil { + respData.Web.Status = fmt.Sprintf("%v", err) + } + } + + if reqData.DNS.Port != 0 { + err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port) + + if errorIsAddrInUse(err) { + canAutofix := checkDNSStubListener() + if canAutofix && reqData.DNS.Autofix { + + err = disableDNSStubListener() + if err != nil { + log.Error("Couldn't disable DNSStubListener: %s", err) + } + + err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port) + canAutofix = false + } + + respData.DNS.CanAutofix = canAutofix + } + + if err == nil { + err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port) + } + + if err != nil { + respData.DNS.Status = fmt.Sprintf("%v", err) + } + } + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(respData) + if err != nil { + httpError(w, http.StatusInternalServerError, "Unable to marshal JSON: %s", err) + return + } +} + +// Check if DNSStubListener is active +func checkDNSStubListener() bool { + cmd := exec.Command("systemctl", "is-enabled", "systemd-resolved") + log.Tracef("executing %s %v", cmd.Path, cmd.Args) + _, err := cmd.Output() + if err != nil || cmd.ProcessState.ExitCode() != 0 { + log.Error("command %s has failed: %v code:%d", + cmd.Path, err, cmd.ProcessState.ExitCode()) + return false + } + + cmd = exec.Command("grep", "-E", "#?DNSStubListener=yes", "/etc/systemd/resolved.conf") + log.Tracef("executing %s %v", cmd.Path, cmd.Args) + _, err = cmd.Output() + if err != nil || cmd.ProcessState.ExitCode() != 0 { + log.Error("command %s has failed: %v code:%d", + cmd.Path, err, cmd.ProcessState.ExitCode()) + return false + } + + return true +} + +// Deactivate DNSStubListener +func disableDNSStubListener() error { + cmd := exec.Command("sed", "-r", "-i.orig", "s/#?DNSStubListener=yes/DNSStubListener=no/g", "/etc/systemd/resolved.conf") + log.Tracef("executing %s %v", cmd.Path, cmd.Args) + _, err := cmd.Output() + if err != nil { + return err + } + if cmd.ProcessState.ExitCode() != 0 { + return fmt.Errorf("process %s exited with an error: %d", + cmd.Path, cmd.ProcessState.ExitCode()) + } + + cmd = exec.Command("systemctl", "reload-or-restart", "systemd-resolved") + log.Tracef("executing %s %v", cmd.Path, cmd.Args) + _, err = cmd.Output() + if err != nil { + return err + } + if cmd.ProcessState.ExitCode() != 0 { + return fmt.Errorf("process %s exited with an error: %d", + cmd.Path, cmd.ProcessState.ExitCode()) + } + + return nil +} + +type applyConfigReqEnt struct { + IP string `json:"ip"` + Port int `json:"port"` +} +type applyConfigReq struct { + Web applyConfigReqEnt `json:"web"` + DNS applyConfigReqEnt `json:"dns"` + Username string `json:"username"` + Password string `json:"password"` +} + +// Copy installation parameters between two configuration objects +func copyInstallSettings(dst *configuration, src *configuration) { + dst.BindHost = src.BindHost + dst.BindPort = src.BindPort + dst.DNS.BindHost = src.DNS.BindHost + dst.DNS.Port = src.DNS.Port + dst.AuthName = src.AuthName + dst.AuthPass = src.AuthPass +} + +// Apply new configuration, start DNS server, restart Web server +func handleInstallConfigure(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + newSettings := applyConfigReq{} + err := json.NewDecoder(r.Body).Decode(&newSettings) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err) + return + } + + if newSettings.Web.Port == 0 || newSettings.DNS.Port == 0 { + httpError(w, http.StatusBadRequest, "port value can't be 0") + return + } + + restartHTTP := true + if config.BindHost == newSettings.Web.IP && config.BindPort == newSettings.Web.Port { + // no need to rebind + restartHTTP = false + } + + // validate that hosts and ports are bindable + if restartHTTP { + err = checkPortAvailable(newSettings.Web.IP, newSettings.Web.Port) + if err != nil { + httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s", + net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err) + return + } + } + + err = checkPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) + if err != nil { + httpError(w, http.StatusBadRequest, "%s", err) + return + } + + err = checkPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) + if err != nil { + httpError(w, http.StatusBadRequest, "%s", err) + return + } + + var curConfig configuration + copyInstallSettings(&curConfig, &config) + + config.firstRun = false + config.BindHost = newSettings.Web.IP + config.BindPort = newSettings.Web.Port + config.DNS.BindHost = newSettings.DNS.IP + config.DNS.Port = newSettings.DNS.Port + config.AuthName = newSettings.Username + config.AuthPass = newSettings.Password + + err = startDNSServer() + if err != nil { + config.firstRun = true + copyInstallSettings(&config, &curConfig) + httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err) + return + } + + err = config.write() + if err != nil { + config.firstRun = true + copyInstallSettings(&config, &curConfig) + httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err) + return + } + + // this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block + // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely + if restartHTTP { + go func() { + httpServer.Shutdown(context.TODO()) + }() + } + + returnOK(w) +} + +func registerInstallHandlers() { + http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses))) + http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(handleInstallCheckConfig))) + http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure))) +}