Pull request: 3172 fix mobileconfig

Merge in DNS/adguard-home from 3172-mobileconfig to master

Updates #3172.
Updates #2497.

Squashed commit of the following:

commit 30549ef4eda9d88f0738089e901492d7369caa25
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jun 1 21:00:17 2021 +0300

    all: log changes

commit 9b9429447430a8e5656b992c04c4a74606dc5f9f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Jun 1 17:56:59 2021 +0300

    client: always show port input

commit 6d6a0bdfaa849220a5ddb4a17502ab05379d7a1c
Merge: 13a3bffd 77946a7f
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jun 1 17:50:41 2021 +0300

    Merge branch 'master' into 3172-mobileconfig

commit 13a3bffd4dd6ccabf3d261f17b2c758a5c61eb9c
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Jun 1 17:20:17 2021 +0300

    client: add port to mobile config form

commit f6abe0b6044572f3801c31b683e76f90c4a28487
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon May 31 19:43:37 2021 +0300

    home: imp cyclo

commit c304a0bacdca6f8b5ffd21f3d00c8244ea9e4e36
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon May 31 18:19:46 2021 +0300

    home: reduce allocs

commit 10a7678861079b710bb0ef14569c60a09612ec70
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon May 24 20:05:08 2021 +0300

    all: make the host parameter required
This commit is contained in:
Eugene Burkov 2021-06-01 21:06:55 +03:00
parent 77946a7f72
commit 3f1fd56b17
17 changed files with 223 additions and 186 deletions

1
.gitignore vendored
View file

@ -9,6 +9,7 @@
*.db *.db
*.log *.log
*.snap *.snap
/agh-backup/
/bin/ /bin/
/build/* /build/*
/build2/* /build2/*

View file

@ -15,6 +15,8 @@ and this project adheres to
### Added ### Added
- Support for custom port in DNS-over-HTTPS profiles for Apple's devices
([#3172]).
- `darwin/arm64` support ([#2443]). - `darwin/arm64` support ([#2443]).
- `freebsd/arm64` support ([#2441]). - `freebsd/arm64` support ([#2441]).
- Output of the default addresses of the upstreams used for resolving PTRs for - 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 [#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441
[#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443 [#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443
[#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136 [#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136
[#3172]: https://github.com/AdguardTeam/AdGuardHome/issues/3172
[#3184]: https://github.com/AdguardTeam/AdGuardHome/issues/3184 [#3184]: https://github.com/AdguardTeam/AdGuardHome/issues/3184
[#3185]: https://github.com/AdguardTeam/AdGuardHome/issues/3185 [#3185]: https://github.com/AdguardTeam/AdGuardHome/issues/3185
[#3186]: https://github.com/AdguardTeam/AdGuardHome/issues/3186 [#3186]: https://github.com/AdguardTeam/AdGuardHome/issues/3186

View file

@ -154,7 +154,8 @@ const getTabs = ({
tlsAddress, tlsAddress,
httpsAddress, httpsAddress,
showDnsPrivacyNotice, showDnsPrivacyNotice,
server_name, serverName,
portHttps,
t, t,
}) => ({ }) => ({
Router: { Router: {
@ -276,9 +277,10 @@ const getTabs = ({
</div> </div>
<MobileConfigForm <MobileConfigForm
initialValues={{ initialValues={{
host: server_name, host: serverName,
clientId: '', clientId: '',
protocol: MOBILE_CONFIG_LINKS.DOH, protocol: MOBILE_CONFIG_LINKS.DOH,
port: portHttps,
}} }}
/> />
</> </>
@ -311,7 +313,8 @@ const renderContent = ({ title, list, getTitle }) => (
const Guide = ({ dnsAddresses }) => { const Guide = ({ dnsAddresses }) => {
const { t } = useTranslation(); 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 tlsAddress = dnsAddresses?.filter((item) => item.includes('tls://')) ?? '';
const httpsAddress = dnsAddresses?.filter((item) => item.includes('https://')) ?? ''; const httpsAddress = dnsAddresses?.filter((item) => item.includes('https://')) ?? '';
const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1; const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1;
@ -322,7 +325,8 @@ const Guide = ({ dnsAddresses }) => {
tlsAddress, tlsAddress,
httpsAddress, httpsAddress,
showDnsPrivacyNotice, showDnsPrivacyNotice,
server_name, serverName,
portHttps,
t, t,
}); });

View file

@ -7,14 +7,17 @@ import i18next from 'i18next';
import cn from 'classnames'; import cn from 'classnames';
import { getPathWithQueryString } from '../../../helpers/helpers'; 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 { import {
renderInputField, renderInputField,
renderSelectField, renderSelectField,
toNumber,
} from '../../../helpers/form'; } from '../../../helpers/form';
import { import {
validateClientId, validateClientId,
validateServerName, validateServerName,
validatePort,
validateIsSafePort,
} from '../../../helpers/validators'; } from '../../../helpers/validators';
const getDownloadLink = (host, clientId, protocol, invalid) => { const getDownloadLink = (host, clientId, protocol, invalid) => {
@ -53,7 +56,9 @@ const MobileConfigForm = ({ invalid }) => {
return null; return null;
} }
const { host, clientId, protocol } = formValues; const {
host, clientId, protocol, port,
} = formValues;
const githubLink = ( const githubLink = (
<a <a
@ -65,21 +70,52 @@ const MobileConfigForm = ({ invalid }) => {
</a> </a>
); );
const getHostName = () => {
if (port
&& port !== STANDARD_HTTPS_PORT
&& protocol === MOBILE_CONFIG_LINKS.DOH
) {
return `${host}:${port}`;
}
return host;
};
return ( return (
<form onSubmit={(e) => e.preventDefault()}> <form onSubmit={(e) => e.preventDefault()}>
<div> <div>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
<label htmlFor="host" className="form__label"> <div className="row">
{i18next.t('dhcp_table_hostname')} <div className="col">
</label> <label htmlFor="host" className="form__label">
<Field {i18next.t('dhcp_table_hostname')}
name="host" </label>
type="text" <Field
component={renderInputField} name="host"
className="form-control" type="text"
placeholder={i18next.t('form_enter_hostname')} component={renderInputField}
validate={validateServerName} className="form-control"
/> placeholder={i18next.t('form_enter_hostname')}
validate={validateServerName}
/>
</div>
{protocol === MOBILE_CONFIG_LINKS.DOH && (
<div className="col">
<label htmlFor="port" className="form__label">
{i18next.t('encryption_https')}
</label>
<Field
name="port"
type="number"
component={renderInputField}
className="form-control"
placeholder={i18next.t('encryption_https')}
validate={[validatePort, validateIsSafePort]}
normalize={toNumber}
/>
</div>
)}
</div>
</div> </div>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
<label htmlFor="clientId" className="form__label form__label--with-desc"> <label htmlFor="clientId" className="form__label form__label--with-desc">
@ -119,7 +155,7 @@ const MobileConfigForm = ({ invalid }) => {
</div> </div>
</div> </div>
{getDownloadLink(host, clientId, protocol, invalid)} {getDownloadLink(getHostName(), clientId, protocol, invalid)}
</form> </form>
); );
}; };

View file

@ -186,7 +186,7 @@ func GetSubnet(ifaceName string) *net.IPNet {
// CheckPortAvailable - check if TCP port is available // CheckPortAvailable - check if TCP port is available
func CheckPortAvailable(host net.IP, port int) error { 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 { if err != nil {
return err return err
} }
@ -200,7 +200,7 @@ func CheckPortAvailable(host net.IP, port int) error {
// CheckPacketPortAvailable - check if UDP port is available // CheckPacketPortAvailable - check if UDP port is available
func CheckPacketPortAvailable(host net.IP, port int) error { 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 { if err != nil {
return err return err
} }
@ -424,3 +424,9 @@ func CollectAllIfacesAddrs() (addrs []string, err error) {
return addrs, nil 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))
}

View file

@ -21,7 +21,8 @@ func CloneSlice(a []string) (b []string) {
// Coalesce returns the first non-empty string. It is named after the function // 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 // 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) { func Coalesce(strs ...string) (res string) {
for _, s := range strs { for _, s := range strs {
if s != "" { if s != "" {

View file

@ -12,6 +12,7 @@ import (
"runtime" "runtime"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
@ -44,7 +45,7 @@ func CheckIfOtherDHCPServersPresentV4(ifaceName string) (ok bool, err error) {
} }
srcIP := ifaceIPNet[0] srcIP := ifaceIPNet[0]
src := net.JoinHostPort(srcIP.String(), "68") src := aghnet.JoinHostPort(srcIP.String(), 68)
dst := "255.255.255.255:67" dst := "255.255.255.255:67"
hostname, _ := os.Hostname() hostname, _ := os.Hostname()
@ -175,7 +176,7 @@ func CheckIfOtherDHCPServersPresentV6(ifaceName string) (ok bool, err error) {
} }
srcIP := ifaceIPNet[0] srcIP := ifaceIPNet[0]
src := net.JoinHostPort(srcIP.String(), "546") src := aghnet.JoinHostPort(srcIP.String(), 546)
dst := "[ff02::1:2]:547" dst := "[ff02::1:2]:547"
req, err := dhcpv6.NewSolicit(iface.HardwareAddr) req, err := dhcpv6.NewSolicit(iface.HardwareAddr)

View file

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"runtime" "runtime"
"strconv"
"strings" "strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "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. // addresses to a slice of strings.
func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) { func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) {
for _, addr := range addrs { for _, addr := range addrs {
hostport := addr.String() var hostport string
if config.DNS.Port != 53 { 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) dst = append(dst, hostport)
@ -303,8 +304,7 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
if r.TLS == nil && web.forceHTTPS { if r.TLS == nil && web.forceHTTPS {
hostPort := host hostPort := host
if port := web.conf.PortHTTPS; port != defaultHTTPSPort { if port := web.conf.PortHTTPS; port != defaultHTTPSPort {
portStr := strconv.Itoa(port) hostPort = aghnet.JoinHostPort(host, port)
hostPort = net.JoinHostPort(host, portStr)
} }
httpsURL := &url.URL{ httpsURL := &url.URL{

View file

@ -11,7 +11,6 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "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 // Apply new configuration, start DNS server, restart Web server
func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
newSettings := applyConfigReq{} req := applyConfigReq{}
err := json.NewDecoder(r.Body).Decode(&newSettings) err := json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err) httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err)
return 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") httpError(w, http.StatusBadRequest, "port value can't be 0")
return return
} }
restartHTTP := true 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 // no need to rebind
restartHTTP = false restartHTTP = false
} }
// validate that hosts and ports are bindable // validate that hosts and ports are bindable
if restartHTTP { if restartHTTP {
err = aghnet.CheckPortAvailable(newSettings.Web.IP, newSettings.Web.Port) err = aghnet.CheckPortAvailable(req.Web.IP, req.Web.Port)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s", httpError(
net.JoinHostPort(newSettings.Web.IP.String(), strconv.Itoa(newSettings.Web.Port)), err) w,
http.StatusBadRequest,
"can not listen on IP:port %s: %s",
aghnet.JoinHostPort(req.Web.IP.String(), req.Web.Port),
err,
)
return return
} }
} }
err = aghnet.CheckPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) err = aghnet.CheckPacketPortAvailable(req.DNS.IP, req.DNS.Port)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "%s", err) httpError(w, http.StatusBadRequest, "%s", err)
return return
} }
err = aghnet.CheckPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) err = aghnet.CheckPortAvailable(req.DNS.IP, req.DNS.Port)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "%s", err) httpError(w, http.StatusBadRequest, "%s", err)
return return
@ -331,10 +336,10 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
copyInstallSettings(&curConfig, &config) copyInstallSettings(&curConfig, &config)
Context.firstRun = false Context.firstRun = false
config.BindHost = newSettings.Web.IP config.BindHost = req.Web.IP
config.BindPort = newSettings.Web.Port config.BindPort = req.Web.Port
config.DNS.BindHosts = []net.IP{newSettings.DNS.IP} config.DNS.BindHosts = []net.IP{req.DNS.IP}
config.DNS.Port = newSettings.DNS.Port config.DNS.Port = req.DNS.Port
// TODO(e.burkov): StartMods() should be put in a separate goroutine at // 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 // 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 := User{}
u.Name = newSettings.Username u.Name = req.Username
Context.auth.UserAdd(&u, newSettings.Password) Context.auth.UserAdd(&u, req.Password)
err = config.write() err = config.write()
if err != nil { if err != nil {
@ -361,8 +366,8 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
} }
web.conf.firstRun = false web.conf.firstRun = false
web.conf.BindHost = newSettings.Web.IP web.conf.BindHost = req.Web.IP
web.conf.BindPort = newSettings.Web.Port web.conf.BindPort = req.Web.Port
registerControlHandlers() registerControlHandlers()

View file

@ -6,7 +6,6 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
@ -254,7 +253,7 @@ func getDNSEncryption() (de dnsEncryption) {
if tlsConf.PortHTTPS != 0 { if tlsConf.PortHTTPS != 0 {
addr := hostname addr := hostname
if tlsConf.PortHTTPS != 443 { if tlsConf.PortHTTPS != 443 {
addr = net.JoinHostPort(addr, strconv.Itoa(tlsConf.PortHTTPS)) addr = aghnet.JoinHostPort(addr, tlsConf.PortHTTPS)
} }
de.https = (&url.URL{ de.https = (&url.URL{
@ -267,14 +266,14 @@ func getDNSEncryption() (de dnsEncryption) {
if tlsConf.PortDNSOverTLS != 0 { if tlsConf.PortDNSOverTLS != 0 {
de.tls = (&url.URL{ de.tls = (&url.URL{
Scheme: "tls", Scheme: "tls",
Host: net.JoinHostPort(hostname, strconv.Itoa(tlsConf.PortDNSOverTLS)), Host: aghnet.JoinHostPort(hostname, tlsConf.PortDNSOverTLS),
}).String() }).String()
} }
if tlsConf.PortDNSOverQUIC != 0 { if tlsConf.PortDNSOverQUIC != 0 {
de.quic = (&url.URL{ de.quic = (&url.URL{
Scheme: "quic", Scheme: "quic",
Host: net.JoinHostPort(hostname, strconv.Itoa(int(tlsConf.PortDNSOverQUIC))), Host: aghnet.JoinHostPort(hostname, tlsConf.PortDNSOverQUIC),
}).String() }).String()
} }
} }

View file

@ -15,7 +15,6 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@ -630,7 +629,28 @@ func loadOptions() options {
return o 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. // admin interface. proto is either schemeHTTP or schemeHTTPS.
func printHTTPAddresses(proto string) { func printHTTPAddresses(proto string) {
tlsConf := tlsConfigSettings{} tlsConf := tlsConfigSettings{}
@ -638,45 +658,40 @@ func printHTTPAddresses(proto string) {
Context.tls.WriteDiskConfig(&tlsConf) Context.tls.WriteDiskConfig(&tlsConf)
} }
port := strconv.Itoa(config.BindPort) port := config.BindPort
if proto == schemeHTTPS { 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 proto == schemeHTTPS && tlsConf.ServerName != "" {
if tlsConf.PortHTTPS == 443 { printWebAddrs(proto, tlsConf.ServerName, tlsConf.PortHTTPS, 0)
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
}
for _, iface := range ifaces { return
for _, addr := range iface.Addresses { }
hostStr = addr.String()
log.Printf("Go to %s://%s", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BindPort))) bindhost := config.BindHost
if config.BetaBindPort != 0 { if !bindhost.IsUnspecified() {
log.Printf("Go to %s://%s (BETA)", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BetaBindPort))) printWebAddrs(proto, bindhost.String(), port, config.BetaBindPort)
}
} return
} }
} else {
hostStr = config.BindHost.String() ifaces, err := aghnet.GetValidNetInterfacesForWeb()
log.Printf("Go to %s://%s", proto, net.JoinHostPort(hostStr, port)) if err != nil {
if config.BetaBindPort != 0 { log.Error("web: getting iface ips: %s", err)
log.Printf("Go to %s://%s (BETA)", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BetaBindPort))) // 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)
} }
} }
} }

View file

@ -8,6 +8,7 @@ import (
"path" "path"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
uuid "github.com/satori/go.uuid" uuid "github.com/satori/go.uuid"
"howett.net/plist" "howett.net/plist"
@ -53,27 +54,27 @@ const (
func getMobileConfig(d dnsSettings) ([]byte, error) { func getMobileConfig(d dnsSettings) ([]byte, error) {
var dspName string var dspName string
switch d.DNSProtocol { switch proto := d.DNSProtocol; proto {
case dnsProtoHTTPS: case dnsProtoHTTPS:
dspName = fmt.Sprintf("%s DoH", d.ServerName) dspName = fmt.Sprintf("%s DoH", d.ServerName)
u := &url.URL{ u := &url.URL{
Scheme: schemeHTTPS, Scheme: schemeHTTPS,
Host: d.ServerName, 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() 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: case dnsProtoTLS:
dspName = fmt.Sprintf("%s DoT", d.ServerName) dspName = fmt.Sprintf("%s DoT", d.ServerName)
if d.clientID != "" { if d.clientID != "" {
d.ServerName = d.clientID + "." + d.ServerName d.ServerName = d.clientID + "." + d.ServerName
} }
default: default:
return nil, fmt.Errorf("bad dns protocol %q", d.DNSProtocol) return nil, fmt.Errorf("bad dns protocol %q", proto)
} }
data := mobileConfig{ data := mobileConfig{
@ -99,25 +100,25 @@ func getMobileConfig(d dnsSettings) ([]byte, error) {
return plist.MarshalIndent(data, plist.XMLFormat, "\t") 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) { func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
var err error var err error
q := r.URL.Query() q := r.URL.Query()
host := q.Get("host") host := q.Get("host")
if host == "" { if host == "" {
host = Context.tls.conf.ServerName respondJSONError(w, http.StatusInternalServerError, string(errEmptyHost))
}
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)
}
return return
} }
@ -126,14 +127,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
if clientID != "" { if clientID != "" {
err = dnsforward.ValidateClientID(clientID) err = dnsforward.ValidateClientID(clientID)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) respondJSONError(w, http.StatusBadRequest, err.Error())
err = json.NewEncoder(w).Encode(&jsonError{
Message: err.Error(),
})
if err != nil {
log.Debug("writing 400 json response: %s", err)
}
return return
} }
@ -147,7 +141,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
mobileconfig, err := getMobileConfig(d) mobileconfig, err := getMobileConfig(d)
if err != nil { if err != nil {
httpError(w, http.StatusInternalServerError, "plist.MarshalIndent: %s", err) respondJSONError(w, http.StatusInternalServerError, err.Error())
return return
} }

View file

@ -1,6 +1,8 @@
package home package home
import ( import (
"bytes"
"encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -13,7 +15,7 @@ import (
func TestHandleMobileConfigDOH(t *testing.T) { func TestHandleMobileConfigDOH(t *testing.T) {
t.Run("success", func(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) 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() w := httptest.NewRecorder()
@ -22,39 +24,11 @@ func TestHandleMobileConfigDOH(t *testing.T) {
var mc mobileConfig var mc mobileConfig
_, err = plist.Unmarshal(w.Body.Bytes(), &mc) _, err = plist.Unmarshal(w.Body.Bytes(), &mc)
require.Nil(t, err) require.NoError(t, err)
require.Len(t, mc.PayloadContent, 1) 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].Name)
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName) 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) 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{}} Context.tls = &TLSMod{conf: tlsConfigSettings{}}
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig", nil) 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() w := httptest.NewRecorder()
handleMobileConfigDOH(w, r) handleMobileConfigDOH(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.JSONEq(t, w.Body.String(), b.String())
}) })
t.Run("client_id", func(t *testing.T) { 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) 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() w := httptest.NewRecorder()
@ -84,12 +65,11 @@ func TestHandleMobileConfigDOH(t *testing.T) {
var mc mobileConfig var mc mobileConfig
_, err = plist.Unmarshal(w.Body.Bytes(), &mc) _, err = plist.Unmarshal(w.Body.Bytes(), &mc)
require.Nil(t, err) require.NoError(t, err)
require.Len(t, mc.PayloadContent, 1) 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].Name)
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName) 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) 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) { func TestHandleMobileConfigDOT(t *testing.T) {
t.Run("success", func(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) 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() w := httptest.NewRecorder()
@ -106,33 +86,7 @@ func TestHandleMobileConfigDOT(t *testing.T) {
var mc mobileConfig var mc mobileConfig
_, err = plist.Unmarshal(w.Body.Bytes(), &mc) _, 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)
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.Len(t, mc.PayloadContent, 1) 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].Name)
@ -147,17 +101,25 @@ func TestHandleMobileConfigDOT(t *testing.T) {
Context.tls = &TLSMod{conf: tlsConfigSettings{}} Context.tls = &TLSMod{conf: tlsConfigSettings{}}
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil) 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() w := httptest.NewRecorder()
handleMobileConfigDOT(w, r) handleMobileConfigDOT(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.JSONEq(t, w.Body.String(), b.String())
}) })
t.Run("client_id", func(t *testing.T) { 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) 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() w := httptest.NewRecorder()
@ -166,7 +128,7 @@ func TestHandleMobileConfigDOT(t *testing.T) {
var mc mobileConfig var mc mobileConfig
_, err = plist.Unmarshal(w.Body.Bytes(), &mc) _, err = plist.Unmarshal(w.Body.Bytes(), &mc)
require.Nil(t, err) require.NoError(t, err)
require.Len(t, mc.PayloadContent, 1) 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].Name)

View file

@ -243,7 +243,8 @@ func handleServiceInstallCommand(s service.Service) {
log.Printf(`Almost ready! log.Printf(`Almost ready!
AdGuard Home is successfully installed and will automatically start on boot. 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. 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) printHTTPAddresses(schemeHTTP)
} }
} }

View file

@ -6,7 +6,6 @@ import (
"io/fs" "io/fs"
"net" "net"
"net/http" "net/http"
"strconv"
"sync" "sync"
"time" "time"
@ -162,6 +161,8 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings)
// Start - start serving HTTP requests // Start - start serving HTTP requests
func (web *Web) Start() { func (web *Web) Start() {
log.Println("AdGuard Home is available at the following addresses:")
// for https, we have a separate goroutine loop // for https, we have a separate goroutine loop
go web.tlsServerLoop() 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 // we need to have new instance, because after Shutdown() the Server is not usable
web.httpServer = &http.Server{ web.httpServer = &http.Server{
ErrorLog: log.StdLog("web: plain", log.DEBUG), 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), Handler: withMiddlewares(Context.mux, limitRequestBody),
ReadTimeout: web.conf.ReadTimeout, ReadTimeout: web.conf.ReadTimeout,
ReadHeaderTimeout: web.conf.ReadHeaderTimeout, ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
@ -187,7 +188,7 @@ func (web *Web) Start() {
if web.conf.BetaBindPort != 0 { if web.conf.BetaBindPort != 0 {
web.httpServerBeta = &http.Server{ web.httpServerBeta = &http.Server{
ErrorLog: log.StdLog("web: plain", log.DEBUG), 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), Handler: withMiddlewares(Context.mux, limitRequestBody, web.wrapIndexBeta),
ReadTimeout: web.conf.ReadTimeout, ReadTimeout: web.conf.ReadTimeout,
ReadHeaderTimeout: web.conf.ReadHeaderTimeout, ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
@ -248,7 +249,7 @@ func (web *Web) tlsServerLoop() {
web.httpsServer.cond.L.Unlock() web.httpsServer.cond.L.Unlock()
// prepare HTTPS server // 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{ web.httpsServer.server = &http.Server{
ErrorLog: log.StdLog("web: https", log.DEBUG), ErrorLog: log.StdLog("web: https", log.DEBUG),
Addr: address, Addr: address,

View file

@ -4,6 +4,12 @@
## v0.107: API changes ## 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 field `"default_local_ptr_upstreams"` in `GET /control/dns_info`
* The new optional field `"default_local_ptr_upstreams"` is the list of IP * The new optional field `"default_local_ptr_upstreams"` is the list of IP

View file

@ -1129,6 +1129,7 @@
'example': 'example.org' 'example': 'example.org'
'in': 'query' 'in': 'query'
'name': 'host' 'name': 'host'
'required': true
'schema': 'schema':
'type': 'string' 'type': 'string'
- 'description': > - 'description': >
@ -1163,6 +1164,7 @@
'example': 'example.org' 'example': 'example.org'
'in': 'query' 'in': 'query'
'name': 'host' 'name': 'host'
'required': true
'schema': 'schema':
'type': 'string' 'type': 'string'
- 'description': > - 'description': >