diff --git a/.gitignore b/.gitignore index 0be5afd7..2d3d46fc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.db *.log *.snap +/agh-backup/ /bin/ /build/* /build2/* diff --git a/CHANGELOG.md b/CHANGELOG.md index f9bbb7e8..33984912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to ### Added +- Support for custom port in DNS-over-HTTPS profiles for Apple's devices + ([#3172]). - `darwin/arm64` support ([#2443]). - `freebsd/arm64` support ([#2441]). - Output of the default addresses of the upstreams used for resolving PTRs for @@ -53,6 +55,7 @@ released by then. [#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441 [#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443 [#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136 +[#3172]: https://github.com/AdguardTeam/AdGuardHome/issues/3172 [#3184]: https://github.com/AdguardTeam/AdGuardHome/issues/3184 [#3185]: https://github.com/AdguardTeam/AdGuardHome/issues/3185 [#3186]: https://github.com/AdguardTeam/AdGuardHome/issues/3186 diff --git a/client/src/components/ui/Guide/Guide.js b/client/src/components/ui/Guide/Guide.js index cf1af0b0..53cf82a7 100644 --- a/client/src/components/ui/Guide/Guide.js +++ b/client/src/components/ui/Guide/Guide.js @@ -154,7 +154,8 @@ const getTabs = ({ tlsAddress, httpsAddress, showDnsPrivacyNotice, - server_name, + serverName, + portHttps, t, }) => ({ Router: { @@ -276,9 +277,10 @@ const getTabs = ({ @@ -311,7 +313,8 @@ const renderContent = ({ title, list, getTitle }) => ( const Guide = ({ dnsAddresses }) => { const { t } = useTranslation(); - const server_name = useSelector((state) => state.encryption?.server_name); + const serverName = useSelector((state) => state.encryption?.server_name); + const portHttps = useSelector((state) => state.encryption?.port_https); const tlsAddress = dnsAddresses?.filter((item) => item.includes('tls://')) ?? ''; const httpsAddress = dnsAddresses?.filter((item) => item.includes('https://')) ?? ''; const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1; @@ -322,7 +325,8 @@ const Guide = ({ dnsAddresses }) => { tlsAddress, httpsAddress, showDnsPrivacyNotice, - server_name, + serverName, + portHttps, t, }); diff --git a/client/src/components/ui/Guide/MobileConfigForm.js b/client/src/components/ui/Guide/MobileConfigForm.js index e1726d99..ce9a6942 100644 --- a/client/src/components/ui/Guide/MobileConfigForm.js +++ b/client/src/components/ui/Guide/MobileConfigForm.js @@ -7,14 +7,17 @@ import i18next from 'i18next'; import cn from 'classnames'; import { getPathWithQueryString } from '../../../helpers/helpers'; -import { FORM_NAME, MOBILE_CONFIG_LINKS } from '../../../helpers/constants'; +import { FORM_NAME, MOBILE_CONFIG_LINKS, STANDARD_HTTPS_PORT } from '../../../helpers/constants'; import { renderInputField, renderSelectField, + toNumber, } from '../../../helpers/form'; import { validateClientId, validateServerName, + validatePort, + validateIsSafePort, } from '../../../helpers/validators'; const getDownloadLink = (host, clientId, protocol, invalid) => { @@ -53,7 +56,9 @@ const MobileConfigForm = ({ invalid }) => { return null; } - const { host, clientId, protocol } = formValues; + const { + host, clientId, protocol, port, + } = formValues; const githubLink = ( { ); + const getHostName = () => { + if (port + && port !== STANDARD_HTTPS_PORT + && protocol === MOBILE_CONFIG_LINKS.DOH + ) { + return `${host}:${port}`; + } + + return host; + }; + return (
e.preventDefault()}>
- - +
+
+ + +
+ {protocol === MOBILE_CONFIG_LINKS.DOH && ( +
+ + +
+ )} +
- {getDownloadLink(host, clientId, protocol, invalid)} + {getDownloadLink(getHostName(), clientId, protocol, invalid)}
); }; diff --git a/internal/aghnet/net.go b/internal/aghnet/net.go index ddb26d56..13ace026 100644 --- a/internal/aghnet/net.go +++ b/internal/aghnet/net.go @@ -186,7 +186,7 @@ func GetSubnet(ifaceName string) *net.IPNet { // CheckPortAvailable - check if TCP port is available func CheckPortAvailable(host net.IP, port int) error { - ln, err := net.Listen("tcp", net.JoinHostPort(host.String(), strconv.Itoa(port))) + ln, err := net.Listen("tcp", JoinHostPort(host.String(), port)) if err != nil { return err } @@ -200,7 +200,7 @@ func CheckPortAvailable(host net.IP, port int) error { // CheckPacketPortAvailable - check if UDP port is available func CheckPacketPortAvailable(host net.IP, port int) error { - ln, err := net.ListenPacket("udp", net.JoinHostPort(host.String(), strconv.Itoa(port))) + ln, err := net.ListenPacket("udp", JoinHostPort(host.String(), port)) if err != nil { return err } @@ -424,3 +424,9 @@ func CollectAllIfacesAddrs() (addrs []string, err error) { return addrs, nil } + +// JoinHostPort is a convinient wrapper for net.JoinHostPort with port of type +// int. +func JoinHostPort(host string, port int) (hostport string) { + return net.JoinHostPort(host, strconv.Itoa(port)) +} diff --git a/internal/aghstrings/strings.go b/internal/aghstrings/strings.go index 201a319c..58219f9b 100644 --- a/internal/aghstrings/strings.go +++ b/internal/aghstrings/strings.go @@ -21,7 +21,8 @@ func CloneSlice(a []string) (b []string) { // Coalesce returns the first non-empty string. It is named after the function // COALESCE in SQL except that since strings in Go are non-nullable, it uses an -// empty string as a NULL value. If strs is empty, it returns an empty string. +// empty string as a NULL value. If strs or all it's elements are empty, it +// returns an empty string. func Coalesce(strs ...string) (res string) { for _, s := range strs { if s != "" { diff --git a/internal/dhcpd/checkother.go b/internal/dhcpd/checkother.go index 1e5de1c0..c6345cd7 100644 --- a/internal/dhcpd/checkother.go +++ b/internal/dhcpd/checkother.go @@ -12,6 +12,7 @@ import ( "runtime" "time" + "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/insomniacslk/dhcp/dhcpv4" @@ -44,7 +45,7 @@ func CheckIfOtherDHCPServersPresentV4(ifaceName string) (ok bool, err error) { } srcIP := ifaceIPNet[0] - src := net.JoinHostPort(srcIP.String(), "68") + src := aghnet.JoinHostPort(srcIP.String(), 68) dst := "255.255.255.255:67" hostname, _ := os.Hostname() @@ -175,7 +176,7 @@ func CheckIfOtherDHCPServersPresentV6(ifaceName string) (ok bool, err error) { } srcIP := ifaceIPNet[0] - src := net.JoinHostPort(srcIP.String(), "546") + src := aghnet.JoinHostPort(srcIP.String(), 546) dst := "[ff02::1:2]:547" req, err := dhcpv6.NewSolicit(iface.HardwareAddr) diff --git a/internal/home/control.go b/internal/home/control.go index a4f4301d..06682b46 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" "runtime" - "strconv" "strings" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" @@ -38,9 +37,11 @@ func httpError(w http.ResponseWriter, code int, format string, args ...interface // addresses to a slice of strings. func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) { for _, addr := range addrs { - hostport := addr.String() + var hostport string if config.DNS.Port != 53 { - hostport = net.JoinHostPort(hostport, strconv.Itoa(config.DNS.Port)) + hostport = aghnet.JoinHostPort(addr.String(), config.DNS.Port) + } else { + hostport = addr.String() } dst = append(dst, hostport) @@ -303,8 +304,7 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) { if r.TLS == nil && web.forceHTTPS { hostPort := host if port := web.conf.PortHTTPS; port != defaultHTTPSPort { - portStr := strconv.Itoa(port) - hostPort = net.JoinHostPort(host, portStr) + hostPort = aghnet.JoinHostPort(host, port) } httpsURL := &url.URL{ diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go index 97a0c084..3c9569a4 100644 --- a/internal/home/controlinstall.go +++ b/internal/home/controlinstall.go @@ -11,7 +11,6 @@ import ( "os/exec" "path/filepath" "runtime" - "strconv" "strings" "time" @@ -286,42 +285,48 @@ func shutdownSrv(ctx context.Context, cancel context.CancelFunc, srv *http.Serve // Apply new configuration, start DNS server, restart Web server func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { - newSettings := applyConfigReq{} - err := json.NewDecoder(r.Body).Decode(&newSettings) + req := applyConfigReq{} + err := json.NewDecoder(r.Body).Decode(&req) if err != nil { httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err) return } - if newSettings.Web.Port == 0 || newSettings.DNS.Port == 0 { + if req.Web.Port == 0 || req.DNS.Port == 0 { httpError(w, http.StatusBadRequest, "port value can't be 0") return } restartHTTP := true - if config.BindHost.Equal(newSettings.Web.IP) && config.BindPort == newSettings.Web.Port { + if config.BindHost.Equal(req.Web.IP) && config.BindPort == req.Web.Port { // no need to rebind restartHTTP = false } // validate that hosts and ports are bindable if restartHTTP { - err = aghnet.CheckPortAvailable(newSettings.Web.IP, newSettings.Web.Port) + err = aghnet.CheckPortAvailable(req.Web.IP, req.Web.Port) if err != nil { - httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s", - net.JoinHostPort(newSettings.Web.IP.String(), strconv.Itoa(newSettings.Web.Port)), err) + httpError( + w, + http.StatusBadRequest, + "can not listen on IP:port %s: %s", + aghnet.JoinHostPort(req.Web.IP.String(), req.Web.Port), + err, + ) + return } } - err = aghnet.CheckPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) + err = aghnet.CheckPacketPortAvailable(req.DNS.IP, req.DNS.Port) if err != nil { httpError(w, http.StatusBadRequest, "%s", err) return } - err = aghnet.CheckPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) + err = aghnet.CheckPortAvailable(req.DNS.IP, req.DNS.Port) if err != nil { httpError(w, http.StatusBadRequest, "%s", err) return @@ -331,10 +336,10 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { copyInstallSettings(&curConfig, &config) Context.firstRun = false - config.BindHost = newSettings.Web.IP - config.BindPort = newSettings.Web.Port - config.DNS.BindHosts = []net.IP{newSettings.DNS.IP} - config.DNS.Port = newSettings.DNS.Port + config.BindHost = req.Web.IP + config.BindPort = req.Web.Port + config.DNS.BindHosts = []net.IP{req.DNS.IP} + config.DNS.Port = req.DNS.Port // TODO(e.burkov): StartMods() should be put in a separate goroutine at // the moment we'll allow setting up TLS in the initial configuration or @@ -349,8 +354,8 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { } u := User{} - u.Name = newSettings.Username - Context.auth.UserAdd(&u, newSettings.Password) + u.Name = req.Username + Context.auth.UserAdd(&u, req.Password) err = config.write() if err != nil { @@ -361,8 +366,8 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { } web.conf.firstRun = false - web.conf.BindHost = newSettings.Web.IP - web.conf.BindPort = newSettings.Web.Port + web.conf.BindHost = req.Web.IP + web.conf.BindPort = req.Web.Port registerControlHandlers() diff --git a/internal/home/dns.go b/internal/home/dns.go index 8f4741d3..f4092a95 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -6,7 +6,6 @@ import ( "net/url" "os" "path/filepath" - "strconv" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" @@ -254,7 +253,7 @@ func getDNSEncryption() (de dnsEncryption) { if tlsConf.PortHTTPS != 0 { addr := hostname if tlsConf.PortHTTPS != 443 { - addr = net.JoinHostPort(addr, strconv.Itoa(tlsConf.PortHTTPS)) + addr = aghnet.JoinHostPort(addr, tlsConf.PortHTTPS) } de.https = (&url.URL{ @@ -267,14 +266,14 @@ func getDNSEncryption() (de dnsEncryption) { if tlsConf.PortDNSOverTLS != 0 { de.tls = (&url.URL{ Scheme: "tls", - Host: net.JoinHostPort(hostname, strconv.Itoa(tlsConf.PortDNSOverTLS)), + Host: aghnet.JoinHostPort(hostname, tlsConf.PortDNSOverTLS), }).String() } if tlsConf.PortDNSOverQUIC != 0 { de.quic = (&url.URL{ Scheme: "quic", - Host: net.JoinHostPort(hostname, strconv.Itoa(int(tlsConf.PortDNSOverQUIC))), + Host: aghnet.JoinHostPort(hostname, tlsConf.PortDNSOverQUIC), }).String() } } diff --git a/internal/home/home.go b/internal/home/home.go index f40c9569..7b024440 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -15,7 +15,6 @@ import ( "os/signal" "path/filepath" "runtime" - "strconv" "sync" "syscall" "time" @@ -630,7 +629,28 @@ func loadOptions() options { return o } -// printHTTPAddresses prints the IP addresses which user can use to open the +// printWebAddrs prints addresses built from proto, addr, and an appropriate +// port. At least one address is printed with the value of port. If the value +// of betaPort is 0, the second address is not printed. The output example: +// +// Go to http://127.0.0.1:80 +// Go to http://127.0.0.1:3000 (BETA) +// +func printWebAddrs(proto, addr string, port, betaPort int) { + const ( + hostMsg = "Go to %s://%s" + hostBetaMsg = hostMsg + " (BETA)" + ) + + log.Printf(hostMsg, proto, aghnet.JoinHostPort(addr, port)) + if betaPort == 0 { + return + } + + log.Printf(hostBetaMsg, proto, aghnet.JoinHostPort(addr, config.BetaBindPort)) +} + +// printHTTPAddresses prints the IP addresses which user can use to access the // admin interface. proto is either schemeHTTP or schemeHTTPS. func printHTTPAddresses(proto string) { tlsConf := tlsConfigSettings{} @@ -638,45 +658,40 @@ func printHTTPAddresses(proto string) { Context.tls.WriteDiskConfig(&tlsConf) } - port := strconv.Itoa(config.BindPort) + port := config.BindPort if proto == schemeHTTPS { - port = strconv.Itoa(tlsConf.PortHTTPS) + port = tlsConf.PortHTTPS } - var hostStr string + // TODO(e.burkov): Inspect and perhaps merge with the previous + // condition. if proto == schemeHTTPS && tlsConf.ServerName != "" { - if tlsConf.PortHTTPS == 443 { - log.Printf("Go to https://%s", tlsConf.ServerName) - } else { - log.Printf("Go to https://%s:%s", tlsConf.ServerName, port) - } - } else if config.BindHost.IsUnspecified() { - log.Println("AdGuard Home is available on the following addresses:") - ifaces, err := aghnet.GetValidNetInterfacesForWeb() - if err != nil { - // That's weird, but we'll ignore it - hostStr = config.BindHost.String() - log.Printf("Go to %s://%s", proto, net.JoinHostPort(hostStr, port)) - if config.BetaBindPort != 0 { - log.Printf("Go to %s://%s (BETA)", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BetaBindPort))) - } - return - } + printWebAddrs(proto, tlsConf.ServerName, tlsConf.PortHTTPS, 0) - for _, iface := range ifaces { - for _, addr := range iface.Addresses { - hostStr = addr.String() - log.Printf("Go to %s://%s", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BindPort))) - if config.BetaBindPort != 0 { - log.Printf("Go to %s://%s (BETA)", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BetaBindPort))) - } - } - } - } else { - hostStr = config.BindHost.String() - log.Printf("Go to %s://%s", proto, net.JoinHostPort(hostStr, port)) - if config.BetaBindPort != 0 { - log.Printf("Go to %s://%s (BETA)", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BetaBindPort))) + return + } + + bindhost := config.BindHost + if !bindhost.IsUnspecified() { + printWebAddrs(proto, bindhost.String(), port, config.BetaBindPort) + + return + } + + ifaces, err := aghnet.GetValidNetInterfacesForWeb() + if err != nil { + log.Error("web: getting iface ips: %s", err) + // That's weird, but we'll ignore it. + // + // TODO(e.burkov): Find out when it happens. + printWebAddrs(proto, bindhost.String(), port, config.BetaBindPort) + + return + } + + for _, iface := range ifaces { + for _, addr := range iface.Addresses { + printWebAddrs(proto, addr.String(), config.BindPort, config.BetaBindPort) } } } diff --git a/internal/home/mobileconfig.go b/internal/home/mobileconfig.go index 895512f1..a0380de5 100644 --- a/internal/home/mobileconfig.go +++ b/internal/home/mobileconfig.go @@ -8,6 +8,7 @@ import ( "path" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" + "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" uuid "github.com/satori/go.uuid" "howett.net/plist" @@ -53,27 +54,27 @@ const ( func getMobileConfig(d dnsSettings) ([]byte, error) { var dspName string - switch d.DNSProtocol { + switch proto := d.DNSProtocol; proto { case dnsProtoHTTPS: dspName = fmt.Sprintf("%s DoH", d.ServerName) - u := &url.URL{ Scheme: schemeHTTPS, Host: d.ServerName, - Path: "/dns-query", + Path: path.Join("/dns-query", d.clientID), } - if d.clientID != "" { - u.Path = path.Join(u.Path, d.clientID) - } - d.ServerURL = u.String() + // Empty the ServerName field since it is only must be presented + // in DNS-over-TLS configuration. + // + // See https://developer.apple.com/documentation/devicemanagement/dnssettings/dnssettings. + d.ServerName = "" case dnsProtoTLS: dspName = fmt.Sprintf("%s DoT", d.ServerName) if d.clientID != "" { d.ServerName = d.clientID + "." + d.ServerName } default: - return nil, fmt.Errorf("bad dns protocol %q", d.DNSProtocol) + return nil, fmt.Errorf("bad dns protocol %q", proto) } data := mobileConfig{ @@ -99,25 +100,25 @@ func getMobileConfig(d dnsSettings) ([]byte, error) { return plist.MarshalIndent(data, plist.XMLFormat, "\t") } +func respondJSONError(w http.ResponseWriter, status int, msg string) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(&jsonError{ + Message: msg, + }) + if err != nil { + log.Debug("writing %d json response: %s", status, err) + } +} + +const errEmptyHost errors.Error = "no host in query parameters and no server_name" + func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) { var err error q := r.URL.Query() host := q.Get("host") if host == "" { - host = Context.tls.conf.ServerName - } - - if host == "" { - w.WriteHeader(http.StatusInternalServerError) - - const msg = "no host in query parameters and no server_name" - err = json.NewEncoder(w).Encode(&jsonError{ - Message: msg, - }) - if err != nil { - log.Debug("writing 500 json response: %s", err) - } + respondJSONError(w, http.StatusInternalServerError, string(errEmptyHost)) return } @@ -126,14 +127,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) { if clientID != "" { err = dnsforward.ValidateClientID(clientID) if err != nil { - w.WriteHeader(http.StatusBadRequest) - - err = json.NewEncoder(w).Encode(&jsonError{ - Message: err.Error(), - }) - if err != nil { - log.Debug("writing 400 json response: %s", err) - } + respondJSONError(w, http.StatusBadRequest, err.Error()) return } @@ -147,7 +141,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) { mobileconfig, err := getMobileConfig(d) if err != nil { - httpError(w, http.StatusInternalServerError, "plist.MarshalIndent: %s", err) + respondJSONError(w, http.StatusInternalServerError, err.Error()) return } diff --git a/internal/home/mobileconfig_test.go b/internal/home/mobileconfig_test.go index 2a0e6d43..e902d5e3 100644 --- a/internal/home/mobileconfig_test.go +++ b/internal/home/mobileconfig_test.go @@ -1,6 +1,8 @@ package home import ( + "bytes" + "encoding/json" "net/http" "net/http/httptest" "testing" @@ -13,7 +15,7 @@ import ( func TestHandleMobileConfigDOH(t *testing.T) { t.Run("success", func(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig?host=example.org", nil) - require.Nil(t, err) + require.NoError(t, err) w := httptest.NewRecorder() @@ -22,39 +24,11 @@ func TestHandleMobileConfigDOH(t *testing.T) { var mc mobileConfig _, err = plist.Unmarshal(w.Body.Bytes(), &mc) - require.Nil(t, err) + require.NoError(t, err) require.Len(t, mc.PayloadContent, 1) assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name) assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName) - assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName) - assert.Equal(t, "https://example.org/dns-query", mc.PayloadContent[0].DNSSettings.ServerURL) - }) - - t.Run("success_no_host", func(t *testing.T) { - oldTLSConf := Context.tls - t.Cleanup(func() { Context.tls = oldTLSConf }) - - Context.tls = &TLSMod{ - conf: tlsConfigSettings{ServerName: "example.org"}, - } - - r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig", nil) - require.Nil(t, err) - - w := httptest.NewRecorder() - - handleMobileConfigDOH(w, r) - require.Equal(t, http.StatusOK, w.Code) - - var mc mobileConfig - _, err = plist.Unmarshal(w.Body.Bytes(), &mc) - require.Nil(t, err) - - require.Len(t, mc.PayloadContent, 1) - assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name) - assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName) - assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName) assert.Equal(t, "https://example.org/dns-query", mc.PayloadContent[0].DNSSettings.ServerURL) }) @@ -65,17 +39,24 @@ func TestHandleMobileConfigDOH(t *testing.T) { Context.tls = &TLSMod{conf: tlsConfigSettings{}} r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig", nil) - require.Nil(t, err) + require.NoError(t, err) + + b := &bytes.Buffer{} + err = json.NewEncoder(b).Encode(&jsonError{ + Message: errEmptyHost.Error(), + }) + require.NoError(t, err) w := httptest.NewRecorder() handleMobileConfigDOH(w, r) assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.JSONEq(t, w.Body.String(), b.String()) }) t.Run("client_id", func(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig?host=example.org&client_id=cli42", nil) - require.Nil(t, err) + require.NoError(t, err) w := httptest.NewRecorder() @@ -84,12 +65,11 @@ func TestHandleMobileConfigDOH(t *testing.T) { var mc mobileConfig _, err = plist.Unmarshal(w.Body.Bytes(), &mc) - require.Nil(t, err) + require.NoError(t, err) require.Len(t, mc.PayloadContent, 1) assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name) assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName) - assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName) assert.Equal(t, "https://example.org/dns-query/cli42", mc.PayloadContent[0].DNSSettings.ServerURL) }) } @@ -97,7 +77,7 @@ func TestHandleMobileConfigDOH(t *testing.T) { func TestHandleMobileConfigDOT(t *testing.T) { t.Run("success", func(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig?host=example.org", nil) - require.Nil(t, err) + require.NoError(t, err) w := httptest.NewRecorder() @@ -106,33 +86,7 @@ func TestHandleMobileConfigDOT(t *testing.T) { var mc mobileConfig _, err = plist.Unmarshal(w.Body.Bytes(), &mc) - require.Nil(t, err) - - require.Len(t, mc.PayloadContent, 1) - assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name) - assert.Equal(t, "example.org DoT", mc.PayloadContent[0].PayloadDisplayName) - assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName) - }) - - t.Run("success_no_host", func(t *testing.T) { - oldTLSConf := Context.tls - t.Cleanup(func() { Context.tls = oldTLSConf }) - - Context.tls = &TLSMod{ - conf: tlsConfigSettings{ServerName: "example.org"}, - } - - r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil) - require.Nil(t, err) - - w := httptest.NewRecorder() - - handleMobileConfigDOT(w, r) - require.Equal(t, http.StatusOK, w.Code) - - var mc mobileConfig - _, err = plist.Unmarshal(w.Body.Bytes(), &mc) - require.Nil(t, err) + require.NoError(t, err) require.Len(t, mc.PayloadContent, 1) assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name) @@ -147,17 +101,25 @@ func TestHandleMobileConfigDOT(t *testing.T) { Context.tls = &TLSMod{conf: tlsConfigSettings{}} r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil) - require.Nil(t, err) + require.NoError(t, err) + + b := &bytes.Buffer{} + err = json.NewEncoder(b).Encode(&jsonError{ + Message: errEmptyHost.Error(), + }) + require.NoError(t, err) w := httptest.NewRecorder() handleMobileConfigDOT(w, r) assert.Equal(t, http.StatusInternalServerError, w.Code) + + assert.JSONEq(t, w.Body.String(), b.String()) }) t.Run("client_id", func(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig?host=example.org&client_id=cli42", nil) - require.Nil(t, err) + require.NoError(t, err) w := httptest.NewRecorder() @@ -166,7 +128,7 @@ func TestHandleMobileConfigDOT(t *testing.T) { var mc mobileConfig _, err = plist.Unmarshal(w.Body.Bytes(), &mc) - require.Nil(t, err) + require.NoError(t, err) require.Len(t, mc.PayloadContent, 1) assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name) diff --git a/internal/home/service.go b/internal/home/service.go index d881e9f6..c1b88af9 100644 --- a/internal/home/service.go +++ b/internal/home/service.go @@ -243,7 +243,8 @@ func handleServiceInstallCommand(s service.Service) { log.Printf(`Almost ready! AdGuard Home is successfully installed and will automatically start on boot. There are a few more things that must be configured before you can use it. -Click on the link below and follow the Installation Wizard steps to finish setup.`) +Click on the link below and follow the Installation Wizard steps to finish setup. +AdGuard Home is now available at the following addresses:`) printHTTPAddresses(schemeHTTP) } } diff --git a/internal/home/web.go b/internal/home/web.go index a987a835..a22c6bd0 100644 --- a/internal/home/web.go +++ b/internal/home/web.go @@ -6,7 +6,6 @@ import ( "io/fs" "net" "net/http" - "strconv" "sync" "time" @@ -162,6 +161,8 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) // Start - start serving HTTP requests func (web *Web) Start() { + log.Println("AdGuard Home is available at the following addresses:") + // for https, we have a separate goroutine loop go web.tlsServerLoop() @@ -174,7 +175,7 @@ func (web *Web) Start() { // we need to have new instance, because after Shutdown() the Server is not usable web.httpServer = &http.Server{ ErrorLog: log.StdLog("web: plain", log.DEBUG), - Addr: net.JoinHostPort(hostStr, strconv.Itoa(web.conf.BindPort)), + Addr: aghnet.JoinHostPort(hostStr, web.conf.BindPort), Handler: withMiddlewares(Context.mux, limitRequestBody), ReadTimeout: web.conf.ReadTimeout, ReadHeaderTimeout: web.conf.ReadHeaderTimeout, @@ -187,7 +188,7 @@ func (web *Web) Start() { if web.conf.BetaBindPort != 0 { web.httpServerBeta = &http.Server{ ErrorLog: log.StdLog("web: plain", log.DEBUG), - Addr: net.JoinHostPort(hostStr, strconv.Itoa(web.conf.BetaBindPort)), + Addr: aghnet.JoinHostPort(hostStr, web.conf.BetaBindPort), Handler: withMiddlewares(Context.mux, limitRequestBody, web.wrapIndexBeta), ReadTimeout: web.conf.ReadTimeout, ReadHeaderTimeout: web.conf.ReadHeaderTimeout, @@ -248,7 +249,7 @@ func (web *Web) tlsServerLoop() { web.httpsServer.cond.L.Unlock() // prepare HTTPS server - address := net.JoinHostPort(web.conf.BindHost.String(), strconv.Itoa(web.conf.PortHTTPS)) + address := aghnet.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS) web.httpsServer.server = &http.Server{ ErrorLog: log.StdLog("web: https", log.DEBUG), Addr: address, diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index e00bd39e..2f34c442 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -4,6 +4,12 @@ ## v0.107: API changes +### The parameter `"host"` in `GET /apple/*.mobileconfig` is now required. + +* The parameter `"host"` in `GET` requests for `/apple/doh.mobileconfig` and + `/apple/doh.mobileconfig` is now required to prevent unexpected server name's + value. + ### The new field `"default_local_ptr_upstreams"` in `GET /control/dns_info` * The new optional field `"default_local_ptr_upstreams"` is the list of IP diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 90e25992..07194338 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1129,6 +1129,7 @@ 'example': 'example.org' 'in': 'query' 'name': 'host' + 'required': true 'schema': 'type': 'string' - 'description': > @@ -1163,6 +1164,7 @@ 'example': 'example.org' 'in': 'query' 'name': 'host' + 'required': true 'schema': 'type': 'string' - 'description': >