mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2025-04-15 13:53:36 +03:00
362 lines
9.6 KiB
Go
362 lines
9.6 KiB
Go
package home
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/google/go-cmp/cmp"
|
|
)
|
|
|
|
// Encryption Settings HTTP API
|
|
|
|
// tlsConfigStatus contains the status of a certificate chain and key pair.
|
|
type tlsConfigStatus struct {
|
|
// Subject is the subject of the first certificate in the chain.
|
|
Subject string `json:"subject,omitempty"`
|
|
|
|
// Issuer is the issuer of the first certificate in the chain.
|
|
Issuer string `json:"issuer,omitempty"`
|
|
|
|
// KeyType is the type of the private key.
|
|
KeyType string `json:"key_type,omitempty"`
|
|
|
|
// NotBefore is the NotBefore field of the first certificate in the chain.
|
|
NotBefore time.Time `json:"not_before,omitempty"`
|
|
|
|
// NotAfter is the NotAfter field of the first certificate in the chain.
|
|
NotAfter time.Time `json:"not_after,omitempty"`
|
|
|
|
// WarningValidation is a validation warning message with the issue
|
|
// description.
|
|
WarningValidation string `json:"warning_validation,omitempty"`
|
|
|
|
// DNSNames is the value of SubjectAltNames field of the first certificate
|
|
// in the chain.
|
|
DNSNames []string `json:"dns_names"`
|
|
|
|
// ValidCert is true if the specified certificate chain is a valid chain of
|
|
// X509 certificates.
|
|
ValidCert bool `json:"valid_cert"`
|
|
|
|
// ValidChain is true if the specified certificate chain is verified and
|
|
// issued by a known CA.
|
|
ValidChain bool `json:"valid_chain"`
|
|
|
|
// ValidKey is true if the key is a valid private key.
|
|
ValidKey bool `json:"valid_key"`
|
|
|
|
// ValidPair is true if both certificate and private key are correct for
|
|
// each other.
|
|
ValidPair bool `json:"valid_pair"`
|
|
}
|
|
|
|
// tlsConfigResp is the TLS configuration and status response.
|
|
type tlsConfigResp struct {
|
|
*tlsConfigStatus
|
|
*tlsConfiguration
|
|
|
|
// PrivateKeySaved is true if the private key is saved as a string and omit
|
|
// key from answer.
|
|
PrivateKeySaved bool `yaml:"-" json:"private_key_saved"`
|
|
}
|
|
|
|
// tlsConfigReq is the TLS configuration request.
|
|
type tlsConfigReq struct {
|
|
tlsConfiguration
|
|
|
|
// PrivateKeySaved is true if the private key is saved as a string and omit
|
|
// key from answer.
|
|
PrivateKeySaved bool `yaml:"-" json:"private_key_saved"`
|
|
}
|
|
|
|
// handleTLSStatus is the handler for the GET /control/tls/status HTTP API.
|
|
func (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
|
|
var resp *tlsConfigResp
|
|
func() {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
resp = &tlsConfigResp{
|
|
tlsConfigStatus: m.status,
|
|
tlsConfiguration: m.conf.cloneForEncoding(),
|
|
}
|
|
}()
|
|
|
|
marshalTLS(w, r, resp)
|
|
}
|
|
|
|
// handleTLSValidate is the handler for the POST /control/tls/validate HTTP API.
|
|
func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
|
req, err := unmarshalTLS(r)
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
|
|
|
return
|
|
}
|
|
|
|
if req.PrivateKeySaved {
|
|
req.PrivateKey = m.confForEncoding().PrivateKey
|
|
}
|
|
|
|
if req.Enabled {
|
|
err = validatePorts(
|
|
tcpPort(config.BindPort),
|
|
tcpPort(config.BetaBindPort),
|
|
tcpPort(req.PortHTTPS),
|
|
tcpPort(req.PortDNSOverTLS),
|
|
tcpPort(req.PortDNSCrypt),
|
|
udpPort(config.DNS.Port),
|
|
udpPort(req.PortDNSOverQUIC),
|
|
)
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
if !webCheckPortAvailable(req.PortHTTPS) {
|
|
aghhttp.Error(
|
|
r,
|
|
w,
|
|
http.StatusBadRequest,
|
|
"port %d is not available, cannot enable HTTPS on it",
|
|
req.PortHTTPS,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
resp := &tlsConfigResp{
|
|
tlsConfigStatus: &tlsConfigStatus{},
|
|
tlsConfiguration: &req.tlsConfiguration,
|
|
}
|
|
|
|
// Skip the error check, since we are only interested in the value of
|
|
// resl.tlsConfigStatus.WarningValidation.
|
|
_ = loadTLSConf(resp.tlsConfiguration, resp.tlsConfigStatus)
|
|
|
|
marshalTLS(w, r, resp)
|
|
}
|
|
|
|
// validatePorts validates the uniqueness of TCP and UDP ports for AdGuard Home
|
|
// DNS protocols.
|
|
func validatePorts(
|
|
bindPort, betaBindPort, dohPort, dotPort, dnscryptTCPPort tcpPort,
|
|
dnsPort, doqPort udpPort,
|
|
) (err error) {
|
|
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
|
addPorts(
|
|
tcpPorts,
|
|
tcpPort(bindPort),
|
|
tcpPort(betaBindPort),
|
|
tcpPort(dohPort),
|
|
tcpPort(dotPort),
|
|
tcpPort(dnscryptTCPPort),
|
|
)
|
|
|
|
err = tcpPorts.Validate()
|
|
if err != nil {
|
|
return fmt.Errorf("validating tcp ports: %w", err)
|
|
}
|
|
|
|
udpPorts := aghalg.UniqChecker[udpPort]{}
|
|
addPorts(udpPorts, udpPort(dnsPort), udpPort(doqPort))
|
|
|
|
err = udpPorts.Validate()
|
|
if err != nil {
|
|
return fmt.Errorf("validating udp ports: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleTLSConfigure is the handler for the POST /control/tls/configure HTTP
|
|
// API.
|
|
func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
|
req, err := unmarshalTLS(r)
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
|
|
|
return
|
|
}
|
|
|
|
if req.PrivateKeySaved {
|
|
req.PrivateKey = m.confForEncoding().PrivateKey
|
|
}
|
|
|
|
if req.Enabled {
|
|
err = validatePorts(
|
|
tcpPort(config.BindPort),
|
|
tcpPort(config.BetaBindPort),
|
|
tcpPort(req.PortHTTPS),
|
|
tcpPort(req.PortDNSOverTLS),
|
|
tcpPort(req.PortDNSCrypt),
|
|
udpPort(config.DNS.Port),
|
|
udpPort(req.PortDNSOverQUIC),
|
|
)
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
// TODO(e.burkov): Investigate and perhaps check other ports.
|
|
if !webCheckPortAvailable(req.PortHTTPS) {
|
|
aghhttp.Error(
|
|
r,
|
|
w,
|
|
http.StatusBadRequest,
|
|
"port %d is not available, cannot enable https on it",
|
|
req.PortHTTPS,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
resp := &tlsConfigResp{
|
|
tlsConfigStatus: &tlsConfigStatus{},
|
|
tlsConfiguration: &req.tlsConfiguration,
|
|
}
|
|
err = loadTLSConf(resp.tlsConfiguration, resp.tlsConfigStatus)
|
|
if err != nil {
|
|
marshalTLS(w, r, resp)
|
|
|
|
return
|
|
}
|
|
|
|
restartRequired := m.setConf(resp)
|
|
onConfigModified()
|
|
|
|
err = reconfigureDNSServer()
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
|
|
|
return
|
|
}
|
|
|
|
resp.tlsConfiguration = m.confForEncoding()
|
|
marshalTLS(w, r, resp)
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
|
|
// The background context is used because the TLSConfigChanged wraps context
|
|
// with timeout on its own and shuts down the server, which handles current
|
|
// request. It is also should be done in a separate goroutine due to the
|
|
// same reason.
|
|
if restartRequired {
|
|
go func() {
|
|
Context.web.TLSConfigChanged(context.Background(), resp.tlsConfiguration)
|
|
}()
|
|
}
|
|
}
|
|
|
|
// setConf sets the necessary values from the new configuration.
|
|
func (m *tlsManager) setConf(newConf *tlsConfigResp) (restartRequired bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Reset the DNSCrypt data before comparing, since we currently do not
|
|
// accept these from the frontend.
|
|
//
|
|
// TODO(a.garipov): Define a custom comparer for dnsforward.TLSConfig.
|
|
newConf.DNSCryptConfigFile = m.conf.DNSCryptConfigFile
|
|
newConf.PortDNSCrypt = m.conf.PortDNSCrypt
|
|
if !cmp.Equal(m.conf, newConf, cmp.AllowUnexported(dnsforward.TLSConfig{})) {
|
|
log.Info("tls: config has changed, restarting https server")
|
|
restartRequired = true
|
|
} else {
|
|
log.Info("tls: config has not changed")
|
|
}
|
|
|
|
// Do not just write "m.conf = *newConf.tlsConfiguration", because all other
|
|
// members of m.conf must be preserved.
|
|
m.conf.Enabled = newConf.Enabled
|
|
m.conf.ServerName = newConf.ServerName
|
|
m.conf.ForceHTTPS = newConf.ForceHTTPS
|
|
m.conf.PortHTTPS = newConf.PortHTTPS
|
|
m.conf.PortDNSOverTLS = newConf.PortDNSOverTLS
|
|
m.conf.PortDNSOverQUIC = newConf.PortDNSOverQUIC
|
|
|
|
m.conf.CertificateChain = newConf.CertificateChain
|
|
m.conf.CertificatePath = newConf.CertificatePath
|
|
m.conf.CertificateChainData = newConf.CertificateChainData
|
|
m.conf.PrivateKey = newConf.PrivateKey
|
|
m.conf.PrivateKeyPath = newConf.PrivateKeyPath
|
|
m.conf.PrivateKeyData = newConf.PrivateKeyData
|
|
|
|
m.setCertFileTime()
|
|
|
|
m.status = newConf.tlsConfigStatus
|
|
|
|
return restartRequired
|
|
}
|
|
|
|
// marshalTLS handles Base64-encoded certificates transparently.
|
|
func marshalTLS(w http.ResponseWriter, r *http.Request, conf *tlsConfigResp) {
|
|
if conf.CertificateChain != "" {
|
|
encoded := base64.StdEncoding.EncodeToString([]byte(conf.CertificateChain))
|
|
conf.CertificateChain = encoded
|
|
}
|
|
|
|
if conf.PrivateKey != "" {
|
|
conf.PrivateKeySaved = true
|
|
conf.PrivateKey = ""
|
|
}
|
|
|
|
_ = aghhttp.WriteJSONResponse(w, r, conf)
|
|
}
|
|
|
|
// unmarshalTLS handles Base64-encoded certificates transparently.
|
|
func unmarshalTLS(r *http.Request) (req *tlsConfigReq, err error) {
|
|
req = &tlsConfigReq{}
|
|
err = json.NewDecoder(r.Body).Decode(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing tls config: %w", err)
|
|
}
|
|
|
|
if req.CertificateChain != "" {
|
|
var cert []byte
|
|
cert, err = base64.StdEncoding.DecodeString(req.CertificateChain)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to base64-decode certificate chain: %w", err)
|
|
}
|
|
|
|
req.CertificateChain = string(cert)
|
|
if req.CertificatePath != "" {
|
|
return nil, fmt.Errorf("certificate data and file can't be set together")
|
|
}
|
|
}
|
|
|
|
if req.PrivateKey != "" {
|
|
var key []byte
|
|
key, err = base64.StdEncoding.DecodeString(req.PrivateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to base64-decode private key: %w", err)
|
|
}
|
|
|
|
req.PrivateKey = string(key)
|
|
if req.PrivateKeyPath != "" {
|
|
return nil, fmt.Errorf("private key data and file can't be set together")
|
|
}
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
// registerWebHandlers registers HTTP handlers for TLS configuration.
|
|
func (m *tlsManager) registerWebHandlers() {
|
|
httpRegister(http.MethodGet, "/control/tls/status", m.handleTLSStatus)
|
|
httpRegister(http.MethodPost, "/control/tls/configure", m.handleTLSConfigure)
|
|
httpRegister(http.MethodPost, "/control/tls/validate", m.handleTLSValidate)
|
|
}
|