2019-06-10 08:33:19 +00:00
|
|
|
package home
|
2018-08-30 14:25:33 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2020-02-13 15:42:07 +00:00
|
|
|
"net"
|
2018-08-30 14:25:33 +00:00
|
|
|
"net/http"
|
2020-02-13 15:42:07 +00:00
|
|
|
"net/url"
|
2020-11-16 16:01:12 +00:00
|
|
|
"runtime"
|
2020-02-13 15:42:07 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2021-03-22 13:46:36 +00:00
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
2020-10-30 10:32:02 +00:00
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
2021-01-13 13:18:51 +00:00
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
2019-02-27 15:28:09 +00:00
|
|
|
"github.com/AdguardTeam/golibs/log"
|
2019-04-15 13:21:12 +00:00
|
|
|
"github.com/NYTimes/gziphandler"
|
2018-08-30 14:25:33 +00:00
|
|
|
)
|
|
|
|
|
2019-02-10 17:47:43 +00:00
|
|
|
// ----------------
|
|
|
|
// helper functions
|
|
|
|
// ----------------
|
|
|
|
|
|
|
|
func returnOK(w http.ResponseWriter) {
|
|
|
|
_, err := fmt.Fprintf(w, "OK\n")
|
|
|
|
if err != nil {
|
2019-02-27 14:28:10 +00:00
|
|
|
httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err)
|
2019-02-10 17:47:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func httpError(w http.ResponseWriter, code int, format string, args ...interface{}) {
|
|
|
|
text := fmt.Sprintf(format, args...)
|
2019-02-25 13:44:22 +00:00
|
|
|
log.Info(text)
|
2019-02-10 17:47:43 +00:00
|
|
|
http.Error(w, text, code)
|
|
|
|
}
|
|
|
|
|
2021-03-24 11:52:37 +00:00
|
|
|
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS
|
|
|
|
// addresses to a slice of strings.
|
|
|
|
func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) {
|
|
|
|
for _, addr := range addrs {
|
|
|
|
hostport := addr.String()
|
|
|
|
if config.DNS.Port != 53 {
|
|
|
|
hostport = net.JoinHostPort(hostport, strconv.Itoa(config.DNS.Port))
|
|
|
|
}
|
|
|
|
|
|
|
|
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 addresss.
|
|
|
|
func appendDNSAddrsWithIfaces(dst []string, src []net.IP) (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, net.IP{127, 0, 0, 1})
|
|
|
|
} else {
|
|
|
|
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("collecting dns addresses: %w", err)
|
|
|
|
}
|
2019-07-02 09:19:36 +00:00
|
|
|
}
|
2021-03-24 11:52:37 +00:00
|
|
|
|
|
|
|
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
|
2019-07-02 09:19:36 +00:00
|
|
|
}
|
2019-03-19 11:14:58 +00:00
|
|
|
|
2021-01-21 16:55:41 +00:00
|
|
|
// statusResponse is a response for /control/status endpoint.
|
|
|
|
type statusResponse struct {
|
|
|
|
DNSAddrs []string `json:"dns_addresses"`
|
|
|
|
DNSPort int `json:"dns_port"`
|
|
|
|
HTTPPort int `json:"http_port"`
|
|
|
|
IsProtectionEnabled bool `json:"protection_enabled"`
|
|
|
|
// 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"`
|
|
|
|
Version string `json:"version"`
|
|
|
|
Language string `json:"language"`
|
|
|
|
}
|
|
|
|
|
2021-01-13 14:26:57 +00:00
|
|
|
func handleStatus(w http.ResponseWriter, _ *http.Request) {
|
2021-03-24 11:52:37 +00:00
|
|
|
dnsAddrs, err := collectDNSAddresses()
|
|
|
|
if err != nil {
|
|
|
|
// Don't add a lot of formatting, since the error is already
|
|
|
|
// wrapped by collectDNSAddresses.
|
|
|
|
httpError(w, http.StatusInternalServerError, "%s", err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-17 13:50:02 +00:00
|
|
|
var resp statusResponse
|
|
|
|
|
|
|
|
func() {
|
|
|
|
config.RLock()
|
|
|
|
defer config.RUnlock()
|
|
|
|
|
|
|
|
resp = statusResponse{
|
|
|
|
DNSAddrs: dnsAddrs,
|
|
|
|
DNSPort: config.DNS.Port,
|
|
|
|
HTTPPort: config.BindPort,
|
|
|
|
IsRunning: isRunning(),
|
|
|
|
Version: version.Version(),
|
|
|
|
Language: config.Language,
|
|
|
|
}
|
|
|
|
}()
|
2020-11-16 16:01:12 +00:00
|
|
|
|
2021-01-21 16:55:41 +00:00
|
|
|
var c *dnsforward.FilteringConfig
|
|
|
|
if Context.dnsServer != nil {
|
|
|
|
c = &dnsforward.FilteringConfig{}
|
|
|
|
Context.dnsServer.WriteDiskConfig(c)
|
|
|
|
resp.IsProtectionEnabled = c.ProtectionEnabled
|
2018-08-30 14:25:33 +00:00
|
|
|
}
|
2020-11-16 16:01:12 +00:00
|
|
|
|
2021-01-21 16:55:41 +00:00
|
|
|
// IsDHCPAvailable field is now false by default for Windows.
|
|
|
|
if runtime.GOOS != "windows" {
|
|
|
|
resp.IsDHCPAvailable = Context.dhcpServer != nil
|
2020-11-16 16:01:12 +00:00
|
|
|
}
|
2018-08-30 14:25:33 +00:00
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2021-03-24 11:52:37 +00:00
|
|
|
err = json.NewEncoder(w).Encode(resp)
|
2018-08-30 14:25:33 +00:00
|
|
|
if err != nil {
|
2019-02-27 14:28:10 +00:00
|
|
|
httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
2021-03-24 11:52:37 +00:00
|
|
|
|
2018-08-30 14:25:33 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-21 14:44:07 +00:00
|
|
|
type profileJSON struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
|
|
|
pj := profileJSON{}
|
2020-12-22 18:09:53 +00:00
|
|
|
u := Context.auth.getCurrentUser(r)
|
2019-10-21 14:44:07 +00:00
|
|
|
pj.Name = u.Name
|
|
|
|
|
|
|
|
data, err := json.Marshal(pj)
|
|
|
|
if err != nil {
|
|
|
|
httpError(w, http.StatusInternalServerError, "json.Marshal: %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
_, _ = w.Write(data)
|
|
|
|
}
|
|
|
|
|
2019-02-12 17:02:52 +00:00
|
|
|
// ------------------------
|
|
|
|
// registration of handlers
|
|
|
|
// ------------------------
|
2018-08-30 14:25:33 +00:00
|
|
|
func registerControlHandlers() {
|
2019-08-21 11:39:37 +00:00
|
|
|
httpRegister(http.MethodGet, "/control/status", handleStatus)
|
|
|
|
httpRegister(http.MethodPost, "/control/i18n/change_language", handleI18nChangeLanguage)
|
|
|
|
httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage)
|
2020-11-25 12:50:59 +00:00
|
|
|
Context.mux.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
2019-08-21 11:39:37 +00:00
|
|
|
httpRegister(http.MethodPost, "/control/update", handleUpdate)
|
2020-10-08 08:34:36 +00:00
|
|
|
httpRegister(http.MethodGet, "/control/profile", handleGetProfile)
|
2019-08-21 11:39:37 +00:00
|
|
|
|
2020-10-08 08:34:36 +00:00
|
|
|
// No auth is necessary for DOH/DOT configurations
|
2020-11-25 15:09:41 +00:00
|
|
|
Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDOH))
|
|
|
|
Context.mux.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDOT))
|
2019-08-29 09:34:07 +00:00
|
|
|
RegisterAuthHandlers()
|
2018-08-30 14:25:33 +00:00
|
|
|
}
|
2019-08-21 11:39:37 +00:00
|
|
|
|
2020-12-07 13:04:53 +00:00
|
|
|
func httpRegister(method, url string, handler func(http.ResponseWriter, *http.Request)) {
|
2021-01-13 13:18:51 +00:00
|
|
|
if method == "" {
|
2020-02-27 08:48:59 +00:00
|
|
|
// "/dns-query" handler doesn't need auth, gzip and isn't restricted by 1 HTTP method
|
2020-11-25 12:50:59 +00:00
|
|
|
Context.mux.HandleFunc(url, postInstall(handler))
|
2020-02-27 08:48:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-25 12:50:59 +00:00
|
|
|
Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
|
2019-08-21 11:39:37 +00:00
|
|
|
}
|
2020-02-13 15:42:07 +00:00
|
|
|
|
|
|
|
// ----------------------------------
|
|
|
|
// helper functions for HTTP handlers
|
|
|
|
// ----------------------------------
|
|
|
|
func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
log.Debug("%s %v", r.Method, r.URL)
|
|
|
|
|
|
|
|
if r.Method != method {
|
|
|
|
http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-01-13 14:26:57 +00:00
|
|
|
if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete {
|
2020-02-13 15:42:07 +00:00
|
|
|
Context.controlLock.Lock()
|
|
|
|
defer Context.controlLock.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
handler(w, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
2021-01-13 14:26:57 +00:00
|
|
|
return ensure(http.MethodPost, handler)
|
2020-02-13 15:42:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func ensureGET(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
2021-01-13 14:26:57 +00:00
|
|
|
return ensure(http.MethodGet, handler)
|
2020-02-13 15:42:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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}
|
|
|
|
}
|
|
|
|
|
2021-02-05 11:11:53 +00:00
|
|
|
const defaultHTTPSPort = 443
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-03-22 13:46:36 +00:00
|
|
|
host, err := aghnet.SplitHost(r.Host)
|
2021-02-05 11:11:53 +00:00
|
|
|
if err != nil {
|
2021-03-22 13:46:36 +00:00
|
|
|
httpError(w, http.StatusBadRequest, "bad host: %s", err)
|
2021-02-05 11:11:53 +00:00
|
|
|
|
2021-03-22 13:46:36 +00:00
|
|
|
return false
|
2021-02-05 11:11:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if r.TLS == nil && web.forceHTTPS {
|
|
|
|
hostPort := host
|
|
|
|
if port := web.conf.PortHTTPS; port != defaultHTTPSPort {
|
|
|
|
portStr := strconv.Itoa(port)
|
|
|
|
hostPort = net.JoinHostPort(host, portStr)
|
|
|
|
}
|
|
|
|
|
|
|
|
httpsURL := &url.URL{
|
2021-03-15 11:19:04 +00:00
|
|
|
Scheme: schemeHTTPS,
|
2021-02-05 11:11:53 +00:00
|
|
|
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
|
2021-02-11 15:40:14 +00:00
|
|
|
// 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.
|
2021-02-05 11:11:53 +00:00
|
|
|
originURL := &url.URL{
|
2021-03-15 11:19:04 +00:00
|
|
|
Scheme: schemeHTTP,
|
2021-02-05 11:11:53 +00:00
|
|
|
Host: r.Host,
|
|
|
|
}
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", originURL.String())
|
2021-02-11 15:40:14 +00:00
|
|
|
w.Header().Set("Vary", "Origin")
|
2021-02-05 11:11:53 +00:00
|
|
|
|
|
|
|
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.
|
2020-02-13 15:42:07 +00:00
|
|
|
func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
2021-02-05 11:11:53 +00:00
|
|
|
path := r.URL.Path
|
|
|
|
if Context.firstRun && !strings.HasPrefix(path, "/install.") &&
|
|
|
|
!strings.HasPrefix(path, "/assets/") {
|
2020-02-18 16:27:09 +00:00
|
|
|
http.Redirect(w, r, "/install.html", http.StatusFound)
|
2021-02-05 11:11:53 +00:00
|
|
|
|
2020-02-13 15:42:07 +00:00
|
|
|
return
|
|
|
|
}
|
2020-02-18 16:27:09 +00:00
|
|
|
|
2021-02-05 11:11:53 +00:00
|
|
|
if !handleHTTPSRedirect(w, r) {
|
2020-02-13 15:42:07 +00:00
|
|
|
return
|
|
|
|
}
|
2020-02-18 16:27:09 +00:00
|
|
|
|
2020-02-13 15:42:07 +00:00
|
|
|
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}
|
|
|
|
}
|