mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-16 03:45:45 +03:00
9c48e96939
Merge in DNS/adguard-home from 1333-protection-pause-1 to master Squashed commit of the following: commit 5ff98385bc5ff66e214d80782eb4dc41e344aa38 Merge: 97f94a540bc3ef89
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Fri Mar 24 19:08:21 2023 +0700 Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1 commit 97f94a5498ac221f88f2f7dfef4b255f4945329e Author: Arseny Lisin <a.lisin@adguard.com> Date: Fri Mar 24 13:03:20 2023 +0200 Fix protection timer bugs commit 1cc61af1996bd803f3fa638cb9e2388470072bf0 Merge: 5a144ea3 235ce458 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Mar 23 22:27:47 2023 +0700 Merge remote-tracking branch 'origin/1333-protection-pause-1' into 1333-protection-pause-1 commit 5a144ea3a48c3d0d5e57dd14232ab7a8e77a8c1e Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Mar 23 22:25:08 2023 +0700 dnsforward: imp code commit 235ce458a62b3152f36e32580ed0226a56580ec6 Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Thu Mar 23 17:35:06 2023 +0300 dnsforward: imp locks commit 0ea3a0a176b810a2b3f0b307aa406fe1670c9219 Merge: 52f66810df61741f
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Mar 23 19:30:41 2023 +0700 Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1 # Conflicts: # CHANGELOG.md # openapi/CHANGELOG.md commit 52f668109673286a50909c042e6352cd803e8eed Merge: 9a7eb7b3306c1983
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Mar 23 11:23:50 2023 +0700 Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1 # Conflicts: # CHANGELOG.md # internal/dnsforward/http.go commit 9a7eb7b3ab2b5f6ad321aa3245d33839c3aa6fbd Author: Arseny Lisin <a.lisin@adguard.com> Date: Wed Mar 22 06:56:55 2023 +0200 Review fix commit 5612d51252ba91842bd6811baec1c91136bb3bf2 Merge: c0a918a5c3edab43
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Mar 21 22:00:39 2023 +0700 Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1 # Conflicts: # client/src/__locales/en.json commit c0a918a518ad9b37041aed159d215516258bc987 Author: Arseny Lisin <a.lisin@adguard.com> Date: Tue Mar 21 12:13:18 2023 +0200 Review fix commit 34faa61cc1e6210a612e7a2f4895a1504df37680 Author: Arseny Lisin <a.lisin@adguard.com> Date: Tue Mar 21 10:43:37 2023 +0200 Fix props to new api commit 158e582373863495f0e0ca177d7b365cc66ad671 Author: Arseny Lisin <a.lisin@adguard.com> Date: Mon Mar 20 18:44:34 2023 +0200 Review fix commit 9e8b8c3778b8e1dfad0d39e44f70886dfd3aeb9a Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Mar 20 22:31:28 2023 +0700 all: docs commit 761a203f53b535ca235cfe62f289bd0e02b90be2 Merge: d0b0723148431f8b
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Mar 20 22:26:13 2023 +0700 Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1 commit d0b07231b6f29b534930f1fcfc82b4934c295ff8 Merge: ea448760a2053526
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Mar 13 13:00:52 2023 +0700 Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1 # Conflicts: # CHANGELOG.md # client/src/components/App/index.css # internal/dnsforward/config.go commit ea4487608a9c81d25f155ff63fee7c9dcf21f448 Merge: dfd0f33fa556ce8f
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Feb 21 11:54:27 2023 +0700 Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1 # Conflicts: # CHANGELOG.md commit dfd0f33fb474d497cbc9237ee466276728eea397 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Feb 21 11:51:40 2023 +0700 all: docs commit d36df96fba8c6d923faef85c198b6bd0743b7ee8 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Feb 20 12:41:49 2023 +0700 all: safesearch commit 60f2ceec563221337f34bb60baa96aa2b2429c40 Merge: 7c5144276f6ced33
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Feb 20 12:30:42 2023 +0700 Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1 # Conflicts: # CHANGELOG.md commit 7c514427e77c5b09d8e148c78220a71046e68cd1 Merge: 0fa4ff994d295a38
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Feb 16 11:55:21 2023 +0700 Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1 # Conflicts: # CHANGELOG.md ... and 26 more commits
408 lines
12 KiB
Go
408 lines
12 KiB
Go
package home
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/netip"
|
|
"net/url"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
"github.com/NYTimes/gziphandler"
|
|
)
|
|
|
|
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS
|
|
// addresses to a slice of strings.
|
|
func appendDNSAddrs(dst []string, addrs ...netip.Addr) (res []string) {
|
|
for _, addr := range addrs {
|
|
var hostport string
|
|
if config.DNS.Port != defaultPortDNS {
|
|
hostport = netip.AddrPortFrom(addr, uint16(config.DNS.Port)).String()
|
|
} else {
|
|
hostport = addr.String()
|
|
}
|
|
|
|
dst = append(dst, hostport)
|
|
}
|
|
|
|
return dst
|
|
}
|
|
|
|
// appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to
|
|
// dst. It also adds the IP addresses of all network interfaces if src contains
|
|
// an unspecified IP address.
|
|
func appendDNSAddrsWithIfaces(dst []string, src []netip.Addr) (res []string, err error) {
|
|
ifacesAdded := false
|
|
for _, h := range src {
|
|
if !h.IsUnspecified() {
|
|
dst = appendDNSAddrs(dst, h)
|
|
|
|
continue
|
|
} else if ifacesAdded {
|
|
continue
|
|
}
|
|
|
|
// Add addresses of all network interfaces for addresses like
|
|
// "0.0.0.0" and "::".
|
|
var ifaces []*aghnet.NetInterface
|
|
ifaces, err = aghnet.GetValidNetInterfacesForWeb()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get network interfaces: %w", err)
|
|
}
|
|
|
|
for _, iface := range ifaces {
|
|
dst = appendDNSAddrs(dst, iface.Addresses...)
|
|
}
|
|
|
|
ifacesAdded = true
|
|
}
|
|
|
|
return dst, nil
|
|
}
|
|
|
|
// collectDNSAddresses returns the list of DNS addresses the server is listening
|
|
// on, including the addresses on all interfaces in cases of unspecified IPs.
|
|
func collectDNSAddresses() (addrs []string, err error) {
|
|
if hosts := config.DNS.BindHosts; len(hosts) == 0 {
|
|
addrs = appendDNSAddrs(addrs, netutil.IPv4Localhost())
|
|
} else {
|
|
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("collecting dns addresses: %w", err)
|
|
}
|
|
}
|
|
|
|
de := getDNSEncryption()
|
|
if de.https != "" {
|
|
addrs = append(addrs, de.https)
|
|
}
|
|
|
|
if de.tls != "" {
|
|
addrs = append(addrs, de.tls)
|
|
}
|
|
|
|
if de.quic != "" {
|
|
addrs = append(addrs, de.quic)
|
|
}
|
|
|
|
return addrs, nil
|
|
}
|
|
|
|
// statusResponse is a response for /control/status endpoint.
|
|
type statusResponse struct {
|
|
Version string `json:"version"`
|
|
Language string `json:"language"`
|
|
DNSAddrs []string `json:"dns_addresses"`
|
|
DNSPort int `json:"dns_port"`
|
|
HTTPPort int `json:"http_port"`
|
|
IsProtectionEnabled bool `json:"protection_enabled"`
|
|
// ProtectionDisabledDuration is a pause duration in milliseconds.
|
|
ProtectionDisabledDuration int64 `json:"protection_disabled_duration"`
|
|
// TODO(e.burkov): Inspect if front-end doesn't requires this field as
|
|
// openapi.yaml declares.
|
|
IsDHCPAvailable bool `json:"dhcp_available"`
|
|
IsRunning bool `json:"running"`
|
|
}
|
|
|
|
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|
dnsAddrs, err := collectDNSAddresses()
|
|
if err != nil {
|
|
// Don't add a lot of formatting, since the error is already
|
|
// wrapped by collectDNSAddresses.
|
|
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
|
|
|
return
|
|
}
|
|
|
|
isProtectionEnabled := false
|
|
var c *dnsforward.FilteringConfig
|
|
if Context.dnsServer != nil {
|
|
c = &dnsforward.FilteringConfig{}
|
|
Context.dnsServer.WriteDiskConfig(c)
|
|
isProtectionEnabled = Context.dnsServer.UpdatedProtectionStatus()
|
|
}
|
|
|
|
var resp statusResponse
|
|
func() {
|
|
config.RLock()
|
|
defer config.RUnlock()
|
|
|
|
var pauseDuration int64
|
|
if until := config.DNS.ProtectionDisabledUntil; until != nil {
|
|
pauseDuration = time.Until(*until).Milliseconds()
|
|
}
|
|
|
|
resp = statusResponse{
|
|
Version: version.Version(),
|
|
DNSAddrs: dnsAddrs,
|
|
DNSPort: config.DNS.Port,
|
|
HTTPPort: config.BindPort,
|
|
Language: config.Language,
|
|
IsRunning: isRunning(),
|
|
ProtectionDisabledDuration: pauseDuration,
|
|
IsProtectionEnabled: isProtectionEnabled,
|
|
}
|
|
}()
|
|
|
|
// IsDHCPAvailable field is now false by default for Windows.
|
|
if runtime.GOOS != "windows" {
|
|
resp.IsDHCPAvailable = Context.dhcpServer != nil
|
|
}
|
|
|
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
|
}
|
|
|
|
// ------------------------
|
|
// registration of handlers
|
|
// ------------------------
|
|
func registerControlHandlers() {
|
|
httpRegister(http.MethodGet, "/control/status", handleStatus)
|
|
httpRegister(http.MethodPost, "/control/i18n/change_language", handleI18nChangeLanguage)
|
|
httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage)
|
|
Context.mux.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
|
httpRegister(http.MethodPost, "/control/update", handleUpdate)
|
|
httpRegister(http.MethodGet, "/control/profile", handleGetProfile)
|
|
httpRegister(http.MethodPut, "/control/profile/update", handlePutProfile)
|
|
|
|
// No auth is necessary for DoH/DoT configurations
|
|
Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoH))
|
|
Context.mux.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDoT))
|
|
RegisterAuthHandlers()
|
|
}
|
|
|
|
func httpRegister(method, url string, handler http.HandlerFunc) {
|
|
if method == "" {
|
|
// "/dns-query" handler doesn't need auth, gzip and isn't restricted by 1 HTTP method
|
|
Context.mux.HandleFunc(url, postInstall(handler))
|
|
return
|
|
}
|
|
|
|
Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
|
|
}
|
|
|
|
// ensure returns a wrapped handler that makes sure that the request has the
|
|
// correct method as well as additional method and header checks.
|
|
func ensure(
|
|
method string,
|
|
handler func(http.ResponseWriter, *http.Request),
|
|
) (wrapped func(http.ResponseWriter, *http.Request)) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
m, u := r.Method, r.URL
|
|
log.Debug("started %s %s %s", m, r.Host, u)
|
|
defer func() { log.Debug("finished %s %s %s in %s", m, r.Host, u, time.Since(start)) }()
|
|
|
|
if m != method {
|
|
aghhttp.Error(r, w, http.StatusMethodNotAllowed, "only method %s is allowed", method)
|
|
|
|
return
|
|
}
|
|
|
|
if modifiesData(m) {
|
|
if !ensureContentType(w, r) {
|
|
return
|
|
}
|
|
|
|
Context.controlLock.Lock()
|
|
defer Context.controlLock.Unlock()
|
|
}
|
|
|
|
handler(w, r)
|
|
}
|
|
}
|
|
|
|
// modifiesData returns true if m is an HTTP method that can modify data.
|
|
func modifiesData(m string) (ok bool) {
|
|
return m == http.MethodPost || m == http.MethodPut || m == http.MethodDelete
|
|
}
|
|
|
|
// ensureContentType makes sure that the content type of a data-modifying
|
|
// request is set correctly. If it is not, ensureContentType writes a response
|
|
// to w, and ok is false.
|
|
func ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) {
|
|
const statusUnsup = http.StatusUnsupportedMediaType
|
|
|
|
cType := r.Header.Get(aghhttp.HdrNameContentType)
|
|
if r.ContentLength == 0 {
|
|
if cType == "" {
|
|
return true
|
|
}
|
|
|
|
// Assume that browsers always send a content type when submitting HTML
|
|
// forms and require no content type for requests with no body to make
|
|
// sure that the request comes from JavaScript.
|
|
aghhttp.Error(r, w, statusUnsup, "empty body with content-type %q not allowed", cType)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
const wantCType = aghhttp.HdrValApplicationJSON
|
|
if cType == wantCType {
|
|
return true
|
|
}
|
|
|
|
aghhttp.Error(r, w, statusUnsup, "only content-type %s is allowed", wantCType)
|
|
|
|
return false
|
|
}
|
|
|
|
func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
|
return ensure(http.MethodPost, handler)
|
|
}
|
|
|
|
func ensureGET(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
|
return ensure(http.MethodGet, handler)
|
|
}
|
|
|
|
// Bridge between http.Handler object and Go function
|
|
type httpHandler struct {
|
|
handler func(http.ResponseWriter, *http.Request)
|
|
}
|
|
|
|
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
h.handler(w, r)
|
|
}
|
|
|
|
func ensureHandler(method string, handler func(http.ResponseWriter, *http.Request)) http.Handler {
|
|
h := httpHandler{}
|
|
h.handler = ensure(method, handler)
|
|
return &h
|
|
}
|
|
|
|
// preInstall lets the handler run only if firstRun is true, no redirects
|
|
func preInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !Context.firstRun {
|
|
// if it's not first run, don't let users access it (for example /install.html when configuration is done)
|
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
|
return
|
|
}
|
|
handler(w, r)
|
|
}
|
|
}
|
|
|
|
// preInstallStruct wraps preInstall into a struct that can be returned as an interface where necessary
|
|
type preInstallHandlerStruct struct {
|
|
handler http.Handler
|
|
}
|
|
|
|
func (p *preInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
preInstall(p.handler.ServeHTTP)(w, r)
|
|
}
|
|
|
|
// preInstallHandler returns http.Handler interface for preInstall wrapper
|
|
func preInstallHandler(handler http.Handler) http.Handler {
|
|
return &preInstallHandlerStruct{handler}
|
|
}
|
|
|
|
// handleHTTPSRedirect redirects the request to HTTPS, if needed. If ok is
|
|
// true, the middleware must continue handling the request.
|
|
func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
|
|
web := Context.web
|
|
if web.httpsServer.server == nil {
|
|
return true
|
|
}
|
|
|
|
host, err := netutil.SplitHost(r.Host)
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusBadRequest, "bad host: %s", err)
|
|
|
|
return false
|
|
}
|
|
|
|
var serveHTTP3 bool
|
|
var portHTTPS int
|
|
func() {
|
|
config.RLock()
|
|
defer config.RUnlock()
|
|
|
|
serveHTTP3, portHTTPS = config.DNS.ServeHTTP3, config.TLS.PortHTTPS
|
|
}()
|
|
|
|
respHdr := w.Header()
|
|
|
|
// Let the browser know that server supports HTTP/3.
|
|
//
|
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Alt-Svc.
|
|
//
|
|
// TODO(a.garipov): Consider adding a configurable max-age. Currently, the
|
|
// default is 24 hours.
|
|
if serveHTTP3 {
|
|
altSvc := fmt.Sprintf(`h3=":%d"`, portHTTPS)
|
|
respHdr.Set(aghhttp.HdrNameAltSvc, altSvc)
|
|
}
|
|
|
|
if r.TLS == nil && web.forceHTTPS {
|
|
hostPort := host
|
|
if port := web.conf.PortHTTPS; port != defaultPortHTTPS {
|
|
hostPort = netutil.JoinHostPort(host, port)
|
|
}
|
|
|
|
httpsURL := &url.URL{
|
|
Scheme: aghhttp.SchemeHTTPS,
|
|
Host: hostPort,
|
|
Path: r.URL.Path,
|
|
RawQuery: r.URL.RawQuery,
|
|
}
|
|
http.Redirect(w, r, httpsURL.String(), http.StatusTemporaryRedirect)
|
|
|
|
return false
|
|
}
|
|
|
|
// Allow the frontend from the HTTP origin to send requests to the HTTPS
|
|
// server. This can happen when the user has just set up HTTPS with
|
|
// redirects. Prevent cache-related errors by setting the Vary header.
|
|
//
|
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin.
|
|
originURL := &url.URL{
|
|
Scheme: aghhttp.SchemeHTTP,
|
|
Host: r.Host,
|
|
}
|
|
|
|
respHdr.Set(aghhttp.HdrNameAccessControlAllowOrigin, originURL.String())
|
|
respHdr.Set(aghhttp.HdrNameVary, aghhttp.HdrNameOrigin)
|
|
|
|
return true
|
|
}
|
|
|
|
// postInstall lets the handler to run only if firstRun is false. Otherwise, it
|
|
// redirects to /install.html. It also enforces HTTPS if it is enabled and
|
|
// configured and sets appropriate access control headers.
|
|
func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
path := r.URL.Path
|
|
if Context.firstRun && !strings.HasPrefix(path, "/install.") &&
|
|
!strings.HasPrefix(path, "/assets/") {
|
|
http.Redirect(w, r, "/install.html", http.StatusFound)
|
|
|
|
return
|
|
}
|
|
|
|
if !handleHTTPSRedirect(w, r) {
|
|
return
|
|
}
|
|
|
|
handler(w, r)
|
|
}
|
|
}
|
|
|
|
type postInstallHandlerStruct struct {
|
|
handler http.Handler
|
|
}
|
|
|
|
func (p *postInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
postInstall(p.handler.ServeHTTP)(w, r)
|
|
}
|
|
|
|
func postInstallHandler(handler http.Handler) http.Handler {
|
|
return &postInstallHandlerStruct{handler}
|
|
}
|