2020-10-29 19:39:11 +03:00
|
|
|
// Package home contains AdGuard Home's HTTP API methods.
|
2019-06-10 11:33:19 +03:00
|
|
|
package home
|
2018-08-30 17:25:33 +03:00
|
|
|
|
|
|
|
import (
|
2019-04-25 14:57:03 +03:00
|
|
|
"context"
|
2019-02-12 21:14:02 +03:00
|
|
|
"crypto/tls"
|
2020-03-04 15:11:17 +03:00
|
|
|
"crypto/x509"
|
2018-08-30 17:25:33 +03:00
|
|
|
"fmt"
|
2021-05-21 14:55:42 +03:00
|
|
|
"io/fs"
|
2018-08-30 17:25:33 +03:00
|
|
|
"net"
|
|
|
|
"net/http"
|
2020-11-02 16:32:33 +03:00
|
|
|
"net/http/pprof"
|
2020-03-12 15:11:08 +03:00
|
|
|
"net/url"
|
2018-08-30 17:25:33 +03:00
|
|
|
"os"
|
2018-12-05 15:36:18 +03:00
|
|
|
"os/signal"
|
2018-08-30 17:25:33 +03:00
|
|
|
"path/filepath"
|
2019-02-05 14:09:05 +03:00
|
|
|
"runtime"
|
2019-02-12 21:14:02 +03:00
|
|
|
"sync"
|
2018-12-05 15:36:18 +03:00
|
|
|
"syscall"
|
2019-06-06 03:00:15 +03:00
|
|
|
"time"
|
2018-08-30 17:25:33 +03:00
|
|
|
|
2021-03-16 19:42:15 +03:00
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
2020-10-30 13:32:02 +03:00
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
2021-05-21 16:15:47 +03:00
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
2020-10-30 13:32:02 +03:00
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
2021-01-13 16:18:51 +03:00
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
2021-05-24 17:28:11 +03:00
|
|
|
"github.com/AdguardTeam/golibs/errors"
|
2019-02-25 16:44:22 +03:00
|
|
|
"github.com/AdguardTeam/golibs/log"
|
2020-11-16 19:01:12 +03:00
|
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
2018-08-30 17:25:33 +03:00
|
|
|
)
|
|
|
|
|
2019-02-05 14:09:05 +03:00
|
|
|
const (
|
|
|
|
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
|
|
|
|
configSyslog = "syslog"
|
|
|
|
)
|
|
|
|
|
2019-12-11 12:38:58 +03:00
|
|
|
// Global context
|
|
|
|
type homeContext struct {
|
2020-02-13 18:42:07 +03:00
|
|
|
// Modules
|
|
|
|
// --
|
|
|
|
|
2021-04-12 18:31:45 +03:00
|
|
|
clients clientsContainer // per-client-settings module
|
|
|
|
stats stats.Stats // statistics module
|
|
|
|
queryLog querylog.QueryLog // query log module
|
|
|
|
dnsServer *dnsforward.Server // DNS module
|
|
|
|
rdns *RDNS // rDNS module
|
|
|
|
whois *Whois // WHOIS module
|
2021-05-21 16:15:47 +03:00
|
|
|
dnsFilter *filtering.DNSFilter // DNS filtering module
|
2021-04-12 18:31:45 +03:00
|
|
|
dhcpServer *dhcpd.Server // DHCP module
|
|
|
|
auth *Auth // HTTP authentication module
|
|
|
|
filters Filtering // DNS filtering module
|
|
|
|
web *Web // Web (HTTP, HTTPS) module
|
|
|
|
tls *TLSMod // TLS module
|
|
|
|
etcHosts *aghnet.EtcHostsContainer // IP-hostname pairs taken from system configuration (e.g. /etc/hosts) files
|
2021-01-13 16:18:51 +03:00
|
|
|
updater *updater.Updater
|
2020-02-13 18:42:07 +03:00
|
|
|
|
2021-04-07 20:16:06 +03:00
|
|
|
subnetDetector *aghnet.SubnetDetector
|
2020-11-20 17:32:41 +03:00
|
|
|
|
2020-11-25 15:50:59 +03:00
|
|
|
// mux is our custom http.ServeMux.
|
|
|
|
mux *http.ServeMux
|
|
|
|
|
2020-02-13 18:42:07 +03:00
|
|
|
// Runtime properties
|
|
|
|
// --
|
|
|
|
|
|
|
|
configFilename string // Config filename (can be overridden via the command line arguments)
|
|
|
|
workDir string // Location of our directory, used to protect against CWD being somewhere else
|
|
|
|
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
|
|
|
|
pidFileName string // PID file name. Empty if no PID file was created.
|
|
|
|
disableUpdate bool // If set, don't check for updates
|
|
|
|
controlLock sync.Mutex
|
2020-03-04 15:11:17 +03:00
|
|
|
tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
|
2020-03-23 10:23:34 +03:00
|
|
|
tlsCiphers []uint16 // list of TLS ciphers to use
|
2020-02-13 18:42:07 +03:00
|
|
|
transport *http.Transport
|
|
|
|
client *http.Client
|
|
|
|
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
|
|
|
|
// runningAsService flag is set to true when options are passed from the service runner
|
|
|
|
runningAsService bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// getDataDir returns path to the directory where we store databases and filters
|
|
|
|
func (c *homeContext) getDataDir() string {
|
|
|
|
return filepath.Join(c.workDir, dataDir)
|
2019-12-11 12:38:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Context - a global context object
|
|
|
|
var Context homeContext
|
|
|
|
|
2019-07-09 18:52:18 +03:00
|
|
|
// Main is the entry point
|
2021-05-21 14:55:42 +03:00
|
|
|
func Main(clientBuildFS fs.FS) {
|
2019-02-04 13:54:53 +03:00
|
|
|
// config can be specified, which reads options from there, but other command line flags have to override config values
|
|
|
|
// therefore, we must do it manually instead of using a lib
|
|
|
|
args := loadOptions()
|
2018-08-30 17:25:33 +03:00
|
|
|
|
2020-02-13 18:42:07 +03:00
|
|
|
Context.appSignalChannel = make(chan os.Signal)
|
|
|
|
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
|
|
|
go func() {
|
2020-02-18 14:49:50 +03:00
|
|
|
for {
|
|
|
|
sig := <-Context.appSignalChannel
|
2020-11-05 15:20:57 +03:00
|
|
|
log.Info("Received signal %q", sig)
|
2020-02-18 14:49:50 +03:00
|
|
|
switch sig {
|
|
|
|
case syscall.SIGHUP:
|
|
|
|
Context.clients.Reload()
|
2020-02-19 15:28:06 +03:00
|
|
|
Context.tls.Reload()
|
2020-02-18 14:49:50 +03:00
|
|
|
|
|
|
|
default:
|
2021-01-26 19:44:19 +03:00
|
|
|
cleanup(context.Background())
|
2020-02-18 14:49:50 +03:00
|
|
|
cleanupAlways()
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
}
|
2020-02-13 18:42:07 +03:00
|
|
|
}()
|
|
|
|
|
2020-06-11 10:24:43 +03:00
|
|
|
if args.serviceControlAction != "" {
|
2021-06-03 21:04:13 +03:00
|
|
|
// TODO(a.garipov): github.com/kardianos/service doesn't seem to
|
|
|
|
// support OpenBSD currently. Either patch it to do so or make
|
|
|
|
// our own implementation of the service.System interface.
|
|
|
|
if runtime.GOOS == "openbsd" {
|
|
|
|
log.Fatal("service actions are not supported on openbsd")
|
|
|
|
}
|
|
|
|
|
2021-05-21 14:55:42 +03:00
|
|
|
handleServiceControlAction(args, clientBuildFS)
|
2021-06-03 21:04:13 +03:00
|
|
|
|
2020-06-11 10:24:43 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-02-05 14:09:05 +03:00
|
|
|
// run the protection
|
2021-05-21 14:55:42 +03:00
|
|
|
run(args, clientBuildFS)
|
2019-02-04 13:54:53 +03:00
|
|
|
}
|
|
|
|
|
2020-11-20 17:32:41 +03:00
|
|
|
func setupContext(args options) {
|
2020-02-13 18:42:07 +03:00
|
|
|
Context.runningAsService = args.runningAsService
|
2021-01-25 14:09:29 +03:00
|
|
|
Context.disableUpdate = args.disableUpdate ||
|
|
|
|
version.Channel() == version.ChannelDevelopment
|
2019-02-04 13:54:53 +03:00
|
|
|
|
2020-02-13 18:42:07 +03:00
|
|
|
Context.firstRun = detectFirstRun()
|
|
|
|
if Context.firstRun {
|
2020-04-15 15:17:57 +03:00
|
|
|
log.Info("This is the first time AdGuard Home is launched")
|
2020-06-23 18:02:28 +03:00
|
|
|
checkPermissions()
|
2019-04-01 12:22:54 +03:00
|
|
|
}
|
|
|
|
|
2019-07-09 18:37:24 +03:00
|
|
|
initConfig()
|
2019-05-31 16:39:18 +03:00
|
|
|
|
2021-04-21 18:50:33 +03:00
|
|
|
Context.tlsRoots = LoadSystemRootCAs()
|
|
|
|
Context.tlsCiphers = InitTLSCiphers()
|
2020-03-04 15:11:17 +03:00
|
|
|
Context.transport = &http.Transport{
|
|
|
|
DialContext: customDialContext,
|
|
|
|
Proxy: getHTTPProxy,
|
|
|
|
TLSClientConfig: &tls.Config{
|
2020-11-20 17:32:41 +03:00
|
|
|
RootCAs: Context.tlsRoots,
|
|
|
|
MinVersion: tls.VersionTLS12,
|
2020-03-04 15:11:17 +03:00
|
|
|
},
|
|
|
|
}
|
|
|
|
Context.client = &http.Client{
|
|
|
|
Timeout: time.Minute * 5,
|
|
|
|
Transport: Context.transport,
|
|
|
|
}
|
|
|
|
|
2020-02-13 18:42:07 +03:00
|
|
|
if !Context.firstRun {
|
2019-04-30 14:38:24 +03:00
|
|
|
// Do the upgrade if necessary
|
|
|
|
err := upgradeConfig()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = parseConfig()
|
|
|
|
if err != nil {
|
2021-04-16 10:32:41 +03:00
|
|
|
log.Error("parsing configuration file: %s", err)
|
|
|
|
|
2019-04-30 14:38:24 +03:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2019-04-30 15:26:57 +03:00
|
|
|
|
|
|
|
if args.checkConfig {
|
2021-04-16 10:32:41 +03:00
|
|
|
log.Info("configuration file is ok")
|
|
|
|
|
2019-04-30 15:26:57 +03:00
|
|
|
os.Exit(0)
|
|
|
|
}
|
2019-02-04 13:54:53 +03:00
|
|
|
}
|
2020-11-25 15:50:59 +03:00
|
|
|
|
|
|
|
Context.mux = http.NewServeMux()
|
2020-11-20 17:32:41 +03:00
|
|
|
}
|
2019-02-04 13:54:53 +03:00
|
|
|
|
2021-06-03 21:04:13 +03:00
|
|
|
func setupConfig(args options) (err error) {
|
2020-02-13 18:42:07 +03:00
|
|
|
config.DHCP.WorkDir = Context.workDir
|
2019-10-11 19:56:18 +03:00
|
|
|
config.DHCP.HTTPRegister = httpRegister
|
|
|
|
config.DHCP.ConfigModified = onConfigModified
|
2020-11-16 19:01:12 +03:00
|
|
|
|
|
|
|
Context.dhcpServer = dhcpd.Create(config.DHCP)
|
|
|
|
if Context.dhcpServer == nil {
|
2021-04-29 16:00:07 +03:00
|
|
|
// TODO(a.garipov): There are a lot of places in the code right
|
|
|
|
// now which assume that the DHCP server can be nil despite this
|
|
|
|
// condition. Inspect them and perhaps rewrite them to use
|
|
|
|
// Enabled() instead.
|
2021-06-03 21:04:13 +03:00
|
|
|
return fmt.Errorf("initing dhcp: %w", err)
|
2020-03-13 17:30:09 +03:00
|
|
|
}
|
2020-11-16 19:01:12 +03:00
|
|
|
|
2021-01-13 16:18:51 +03:00
|
|
|
Context.updater = updater.NewUpdater(&updater.Config{
|
|
|
|
Client: Context.client,
|
|
|
|
Version: version.Version(),
|
|
|
|
Channel: version.Channel(),
|
|
|
|
GOARCH: runtime.GOARCH,
|
|
|
|
GOOS: runtime.GOOS,
|
|
|
|
GOARM: version.GOARM(),
|
|
|
|
GOMIPS: version.GOMIPS(),
|
|
|
|
WorkDir: Context.workDir,
|
|
|
|
ConfName: config.getConfigFilename(),
|
2020-07-22 20:27:20 +03:00
|
|
|
})
|
2020-07-22 14:20:14 +03:00
|
|
|
|
2021-04-12 18:31:45 +03:00
|
|
|
if !args.noEtcHosts {
|
|
|
|
Context.etcHosts = &aghnet.EtcHostsContainer{}
|
|
|
|
Context.etcHosts.Init("")
|
|
|
|
}
|
2021-04-14 19:18:48 +03:00
|
|
|
Context.clients.Init(config.Clients, Context.dhcpServer, Context.etcHosts)
|
2019-09-26 16:40:52 +03:00
|
|
|
config.Clients = nil
|
|
|
|
|
2021-06-03 21:04:13 +03:00
|
|
|
if config.RlimitNoFile != 0 {
|
|
|
|
err = aghos.SetRlimit(config.RlimitNoFile)
|
|
|
|
if err != nil && !errors.Is(err, aghos.ErrUnsupported) {
|
|
|
|
return fmt.Errorf("setting rlimit: %w", err)
|
|
|
|
}
|
2019-03-27 17:09:48 +03:00
|
|
|
}
|
|
|
|
|
2019-02-04 13:54:53 +03:00
|
|
|
// override bind host/port from the console
|
2021-01-20 17:27:53 +03:00
|
|
|
if args.bindHost != nil {
|
2019-02-04 13:54:53 +03:00
|
|
|
config.BindHost = args.bindHost
|
|
|
|
}
|
|
|
|
if args.bindPort != 0 {
|
|
|
|
config.BindPort = args.bindPort
|
|
|
|
}
|
2020-02-18 19:27:09 +03:00
|
|
|
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
|
|
|
|
Context.pidFileName = args.pidFile
|
|
|
|
}
|
2021-06-03 21:04:13 +03:00
|
|
|
|
|
|
|
return nil
|
2020-11-20 17:32:41 +03:00
|
|
|
}
|
|
|
|
|
2021-05-21 14:55:42 +03:00
|
|
|
func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
|
|
|
|
var clientFS, clientBetaFS fs.FS
|
|
|
|
if args.localFrontend {
|
|
|
|
log.Info("warning: using local frontend files")
|
|
|
|
|
|
|
|
clientFS = os.DirFS("build/static")
|
|
|
|
clientBetaFS = os.DirFS("build2/static")
|
|
|
|
} else {
|
|
|
|
clientFS, err = fs.Sub(clientBuildFS, "build/static")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("getting embedded client subdir: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
clientBetaFS, err = fs.Sub(clientBuildFS, "build2/static")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("getting embedded beta client subdir: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
webConf := webConfig{
|
|
|
|
firstRun: Context.firstRun,
|
|
|
|
BindHost: config.BindHost,
|
|
|
|
BindPort: config.BindPort,
|
|
|
|
BetaBindPort: config.BetaBindPort,
|
|
|
|
|
|
|
|
ReadTimeout: readTimeout,
|
|
|
|
ReadHeaderTimeout: readHdrTimeout,
|
|
|
|
WriteTimeout: writeTimeout,
|
|
|
|
|
|
|
|
clientFS: clientFS,
|
|
|
|
clientBetaFS: clientBetaFS,
|
|
|
|
}
|
|
|
|
|
|
|
|
web = CreateWeb(&webConf)
|
|
|
|
if web == nil {
|
|
|
|
return nil, fmt.Errorf("initializing web: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return web, nil
|
|
|
|
}
|
|
|
|
|
2021-06-03 21:04:13 +03:00
|
|
|
func fatalOnError(err error) {
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-20 17:32:41 +03:00
|
|
|
// run performs configurating and starts AdGuard Home.
|
2021-05-21 14:55:42 +03:00
|
|
|
func run(args options, clientBuildFS fs.FS) {
|
2021-06-03 21:04:13 +03:00
|
|
|
var err error
|
|
|
|
|
2020-11-20 17:32:41 +03:00
|
|
|
// configure config filename
|
|
|
|
initConfigFilename(args)
|
|
|
|
|
|
|
|
// configure working dir and config path
|
|
|
|
initWorkingDir(args)
|
|
|
|
|
|
|
|
// configure log level and output
|
|
|
|
configureLogger(args)
|
|
|
|
|
|
|
|
// Go memory hacks
|
|
|
|
memoryUsage(args)
|
|
|
|
|
|
|
|
// print the first message after logger is configured
|
2021-01-13 16:18:51 +03:00
|
|
|
log.Println(version.Full())
|
2020-11-20 17:32:41 +03:00
|
|
|
log.Debug("Current working directory is %s", Context.workDir)
|
|
|
|
if args.runningAsService {
|
|
|
|
log.Info("AdGuard Home is running as a service")
|
|
|
|
}
|
|
|
|
|
|
|
|
setupContext(args)
|
|
|
|
|
2021-05-21 16:15:47 +03:00
|
|
|
// clients package uses filtering package's static data (filtering.BlockedSvcKnown()),
|
|
|
|
// so we have to initialize filtering's static data first,
|
2020-11-20 17:32:41 +03:00
|
|
|
// but also avoid relying on automatic Go init() function
|
2021-05-21 16:15:47 +03:00
|
|
|
filtering.InitModule()
|
2020-11-20 17:32:41 +03:00
|
|
|
|
2021-06-03 21:04:13 +03:00
|
|
|
err = setupConfig(args)
|
|
|
|
fatalOnError(err)
|
2018-08-30 17:25:33 +03:00
|
|
|
|
2020-02-13 18:42:07 +03:00
|
|
|
if !Context.firstRun {
|
2019-04-30 14:38:24 +03:00
|
|
|
// Save the updated config
|
2021-06-03 21:04:13 +03:00
|
|
|
err = config.write()
|
|
|
|
fatalOnError(err)
|
2020-04-22 16:00:26 +03:00
|
|
|
|
|
|
|
if config.DebugPProf {
|
|
|
|
mux := http.NewServeMux()
|
2020-11-02 16:32:33 +03:00
|
|
|
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
|
|
|
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
|
|
|
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
|
|
|
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
|
|
|
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
2020-04-22 16:00:26 +03:00
|
|
|
go func() {
|
|
|
|
log.Info("pprof: listening on localhost:6060")
|
2021-03-12 14:32:08 +03:00
|
|
|
lerr := http.ListenAndServe("localhost:6060", mux)
|
|
|
|
log.Error("Error while running the pprof server: %s", lerr)
|
2020-04-22 16:00:26 +03:00
|
|
|
}()
|
|
|
|
}
|
2020-02-18 19:27:09 +03:00
|
|
|
}
|
|
|
|
|
2021-06-03 21:04:13 +03:00
|
|
|
err = os.MkdirAll(Context.getDataDir(), 0o755)
|
2020-02-18 19:27:09 +03:00
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Cannot create DNS data dir at %s: %s", Context.getDataDir(), err)
|
|
|
|
}
|
2018-08-30 17:25:33 +03:00
|
|
|
|
2020-02-19 15:24:55 +03:00
|
|
|
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
2020-07-03 20:34:08 +03:00
|
|
|
GLMode = args.glinetMode
|
2021-04-27 18:56:32 +03:00
|
|
|
var arl *authRateLimiter
|
|
|
|
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
|
|
|
|
arl = newAuthRateLimiter(
|
|
|
|
time.Duration(config.AuthBlockMin)*time.Minute,
|
|
|
|
config.AuthAttempts,
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
log.Info("authratelimiter is disabled")
|
|
|
|
}
|
2021-05-21 14:55:42 +03:00
|
|
|
|
2021-04-27 18:56:32 +03:00
|
|
|
Context.auth = InitAuth(
|
|
|
|
sessFilename,
|
|
|
|
config.Users,
|
|
|
|
config.WebSessionTTLHours*60*60,
|
|
|
|
arl,
|
|
|
|
)
|
2020-02-19 15:24:55 +03:00
|
|
|
if Context.auth == nil {
|
|
|
|
log.Fatalf("Couldn't initialize Auth module")
|
|
|
|
}
|
|
|
|
config.Users = nil
|
|
|
|
|
2020-02-19 15:28:06 +03:00
|
|
|
Context.tls = tlsCreate(config.TLS)
|
|
|
|
if Context.tls == nil {
|
|
|
|
log.Fatalf("Can't initialize TLS module")
|
|
|
|
}
|
|
|
|
|
2021-05-21 14:55:42 +03:00
|
|
|
Context.web, err = initWeb(args, clientBuildFS)
|
2021-06-03 21:04:13 +03:00
|
|
|
fatalOnError(err)
|
2020-02-18 19:27:09 +03:00
|
|
|
|
2021-03-31 15:00:47 +03:00
|
|
|
Context.subnetDetector, err = aghnet.NewSubnetDetector()
|
2021-06-03 21:04:13 +03:00
|
|
|
fatalOnError(err)
|
2021-02-15 14:20:23 +03:00
|
|
|
|
2020-02-18 19:27:09 +03:00
|
|
|
if !Context.firstRun {
|
2021-03-12 14:32:08 +03:00
|
|
|
err = initDNSServer()
|
2021-06-03 21:04:13 +03:00
|
|
|
fatalOnError(err)
|
2021-03-12 14:32:08 +03:00
|
|
|
|
2020-02-19 15:28:06 +03:00
|
|
|
Context.tls.Start()
|
2021-04-12 18:31:45 +03:00
|
|
|
Context.etcHosts.Start()
|
2020-02-19 15:28:06 +03:00
|
|
|
|
2019-10-09 19:51:26 +03:00
|
|
|
go func() {
|
2021-03-12 14:32:08 +03:00
|
|
|
serr := startDNSServer()
|
|
|
|
if serr != nil {
|
2021-02-16 18:46:49 +03:00
|
|
|
closeDNSServer()
|
2021-06-03 21:04:13 +03:00
|
|
|
fatalOnError(serr)
|
2019-10-09 19:51:26 +03:00
|
|
|
}
|
|
|
|
}()
|
2018-09-06 02:00:57 +03:00
|
|
|
|
2020-07-03 18:20:01 +03:00
|
|
|
if Context.dhcpServer != nil {
|
2021-04-21 14:42:19 +03:00
|
|
|
err = Context.dhcpServer.Start()
|
|
|
|
if err != nil {
|
|
|
|
log.Error("starting dhcp server: %s", err)
|
|
|
|
}
|
2019-02-01 19:25:04 +03:00
|
|
|
}
|
2018-12-28 21:01:16 +03:00
|
|
|
}
|
|
|
|
|
2020-02-19 15:24:55 +03:00
|
|
|
Context.web.Start()
|
2020-02-18 19:27:09 +03:00
|
|
|
|
|
|
|
// wait indefinitely for other go-routines to complete their job
|
|
|
|
select {}
|
|
|
|
}
|
|
|
|
|
2021-01-26 19:44:19 +03:00
|
|
|
// StartMods initializes and starts the DNS server after installation.
|
2020-02-19 15:28:06 +03:00
|
|
|
func StartMods() error {
|
|
|
|
err := initDNSServer()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
Context.tls.Start()
|
|
|
|
|
|
|
|
err = startDNSServer()
|
|
|
|
if err != nil {
|
|
|
|
closeDNSServer()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-23 18:04:26 +03:00
|
|
|
// Check if the current user permissions are enough to run AdGuard Home
|
2020-06-23 18:02:28 +03:00
|
|
|
func checkPermissions() {
|
|
|
|
log.Info("Checking if AdGuard Home has necessary permissions")
|
2019-04-01 12:22:54 +03:00
|
|
|
|
|
|
|
if runtime.GOOS == "windows" {
|
2020-06-23 18:02:28 +03:00
|
|
|
// On Windows we need to have admin rights to run properly
|
|
|
|
|
2021-03-16 19:42:15 +03:00
|
|
|
admin, _ := aghos.HaveAdminRights()
|
2020-11-06 20:20:15 +03:00
|
|
|
if admin {
|
2020-06-23 18:02:28 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-04-01 12:22:54 +03:00
|
|
|
log.Fatal("This is the first launch of AdGuard Home. You must run it as Administrator.")
|
2020-06-23 18:02:28 +03:00
|
|
|
}
|
2019-04-01 12:22:54 +03:00
|
|
|
|
2020-06-23 18:02:28 +03:00
|
|
|
// We should check if AdGuard Home is able to bind to port 53
|
2021-03-16 19:42:15 +03:00
|
|
|
ok, err := aghnet.CanBindPort(53)
|
2019-04-01 12:22:54 +03:00
|
|
|
|
2020-06-23 18:02:28 +03:00
|
|
|
if ok {
|
|
|
|
log.Info("AdGuard Home can bind to port 53")
|
|
|
|
return
|
|
|
|
}
|
2019-04-01 12:22:54 +03:00
|
|
|
|
2021-03-12 14:32:08 +03:00
|
|
|
if errors.Is(err, os.ErrPermission) {
|
|
|
|
msg := `Permission check failed.
|
2020-06-23 18:02:28 +03:00
|
|
|
|
|
|
|
AdGuard Home is not allowed to bind to privileged ports (for instance, port 53).
|
|
|
|
Please note, that this is crucial for a server to be able to use privileged ports.
|
|
|
|
|
|
|
|
You have two options:
|
|
|
|
1. Run AdGuard Home with root privileges
|
|
|
|
2. On Linux you can grant the CAP_NET_BIND_SERVICE capability:
|
2020-10-30 13:32:02 +03:00
|
|
|
https://github.com/AdguardTeam/AdGuardHome/internal/wiki/Getting-Started#running-without-superuser`
|
2020-06-23 18:02:28 +03:00
|
|
|
|
2021-03-12 14:32:08 +03:00
|
|
|
log.Fatal(msg)
|
2019-04-01 12:22:54 +03:00
|
|
|
}
|
2020-06-23 18:02:28 +03:00
|
|
|
|
|
|
|
msg := fmt.Sprintf(`AdGuard failed to bind to port 53 due to %v
|
|
|
|
|
|
|
|
Please note, that this is crucial for a DNS server to be able to use that port.`, err)
|
|
|
|
|
|
|
|
log.Info(msg)
|
2019-04-01 12:22:54 +03:00
|
|
|
}
|
|
|
|
|
2019-04-05 12:19:28 +03:00
|
|
|
// Write PID to a file
|
|
|
|
func writePIDFile(fn string) bool {
|
|
|
|
data := fmt.Sprintf("%d", os.Getpid())
|
2021-05-21 14:55:42 +03:00
|
|
|
err := os.WriteFile(fn, []byte(data), 0o644)
|
2019-04-05 12:19:28 +03:00
|
|
|
if err != nil {
|
|
|
|
log.Error("Couldn't write PID to file %s: %v", fn, err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-09-28 10:57:56 +03:00
|
|
|
func initConfigFilename(args options) {
|
|
|
|
// config file path can be overridden by command-line arguments:
|
|
|
|
if args.configFilename != "" {
|
|
|
|
Context.configFilename = args.configFilename
|
|
|
|
} else {
|
|
|
|
// Default config file name
|
|
|
|
Context.configFilename = "AdGuardHome.yaml"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-13 18:42:07 +03:00
|
|
|
// initWorkingDir initializes the workDir
|
2019-02-10 20:47:43 +03:00
|
|
|
// if no command-line arguments specified, we use the directory where our binary file is located
|
2019-02-05 20:35:48 +03:00
|
|
|
func initWorkingDir(args options) {
|
2020-02-11 12:59:21 +03:00
|
|
|
execPath, err := os.Executable()
|
2019-02-05 14:09:05 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2019-02-10 20:47:43 +03:00
|
|
|
if args.workDir != "" {
|
2019-02-05 20:35:48 +03:00
|
|
|
// If there is a custom config file, use it's directory as our working dir
|
2020-02-13 18:42:07 +03:00
|
|
|
Context.workDir = args.workDir
|
2019-02-05 14:09:05 +03:00
|
|
|
} else {
|
2020-02-13 18:42:07 +03:00
|
|
|
Context.workDir = filepath.Dir(execPath)
|
2019-02-05 14:09:05 +03:00
|
|
|
}
|
2021-01-27 18:32:13 +03:00
|
|
|
|
2021-03-12 14:32:08 +03:00
|
|
|
workDir, err := filepath.EvalSymlinks(Context.workDir)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
2021-01-27 18:32:13 +03:00
|
|
|
}
|
2021-03-12 14:32:08 +03:00
|
|
|
|
|
|
|
Context.workDir = workDir
|
2019-02-04 13:54:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// configureLogger configures logger level and output
|
|
|
|
func configureLogger(args options) {
|
|
|
|
ls := getLogSettings()
|
|
|
|
|
|
|
|
// command-line arguments can override config settings
|
2020-06-02 14:20:12 +03:00
|
|
|
if args.verbose || config.Verbose {
|
2019-02-04 13:54:53 +03:00
|
|
|
ls.Verbose = true
|
|
|
|
}
|
|
|
|
if args.logFile != "" {
|
|
|
|
ls.LogFile = args.logFile
|
2020-06-02 14:20:12 +03:00
|
|
|
} else if config.LogFile != "" {
|
|
|
|
ls.LogFile = config.LogFile
|
2019-02-04 13:54:53 +03:00
|
|
|
}
|
|
|
|
|
2020-06-02 14:20:12 +03:00
|
|
|
// Handle default log settings overrides
|
|
|
|
ls.LogCompress = config.LogCompress
|
|
|
|
ls.LogLocalTime = config.LogLocalTime
|
|
|
|
ls.LogMaxBackups = config.LogMaxBackups
|
|
|
|
ls.LogMaxSize = config.LogMaxSize
|
|
|
|
ls.LogMaxAge = config.LogMaxAge
|
|
|
|
|
2019-12-11 12:38:58 +03:00
|
|
|
// log.SetLevel(log.INFO) - default
|
2019-02-25 16:44:22 +03:00
|
|
|
if ls.Verbose {
|
2019-12-11 12:38:58 +03:00
|
|
|
log.SetLevel(log.DEBUG)
|
2019-02-25 16:44:22 +03:00
|
|
|
}
|
2019-02-04 13:54:53 +03:00
|
|
|
|
2021-05-18 14:20:50 +03:00
|
|
|
// Make sure that we see the microseconds in logs, as networking stuff
|
|
|
|
// can happen pretty quickly.
|
|
|
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
|
|
|
|
2019-02-05 14:09:05 +03:00
|
|
|
if args.runningAsService && ls.LogFile == "" && runtime.GOOS == "windows" {
|
|
|
|
// When running as a Windows service, use eventlog by default if nothing else is configured
|
|
|
|
// Otherwise, we'll simply loose the log output
|
|
|
|
ls.LogFile = configSyslog
|
|
|
|
}
|
|
|
|
|
2020-06-02 14:20:12 +03:00
|
|
|
// logs are written to stdout (default)
|
2019-02-04 13:54:53 +03:00
|
|
|
if ls.LogFile == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-02-05 14:09:05 +03:00
|
|
|
if ls.LogFile == configSyslog {
|
|
|
|
// Use syslog where it is possible and eventlog on Windows
|
2021-03-16 19:42:15 +03:00
|
|
|
err := aghos.ConfigureSyslog(serviceName)
|
2019-02-04 13:54:53 +03:00
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("cannot initialize syslog: %s", err)
|
|
|
|
}
|
2019-02-05 14:09:05 +03:00
|
|
|
} else {
|
2020-02-13 18:42:07 +03:00
|
|
|
logFilePath := filepath.Join(Context.workDir, ls.LogFile)
|
2019-03-14 18:06:53 +03:00
|
|
|
if filepath.IsAbs(ls.LogFile) {
|
|
|
|
logFilePath = ls.LogFile
|
|
|
|
}
|
|
|
|
|
2020-11-05 15:20:57 +03:00
|
|
|
_, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
|
2019-02-05 14:09:05 +03:00
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("cannot create a log file: %s", err)
|
|
|
|
}
|
2020-06-02 14:20:12 +03:00
|
|
|
|
|
|
|
log.SetOutput(&lumberjack.Logger{
|
|
|
|
Filename: logFilePath,
|
|
|
|
Compress: ls.LogCompress, // disabled by default
|
|
|
|
LocalTime: ls.LogLocalTime,
|
|
|
|
MaxBackups: ls.LogMaxBackups,
|
|
|
|
MaxSize: ls.LogMaxSize, // megabytes
|
2020-11-05 15:20:57 +03:00
|
|
|
MaxAge: ls.LogMaxAge, // days
|
2020-06-02 14:20:12 +03:00
|
|
|
})
|
2019-02-04 13:54:53 +03:00
|
|
|
}
|
2018-08-30 17:25:33 +03:00
|
|
|
}
|
2018-10-12 19:40:43 +03:00
|
|
|
|
2021-01-26 19:44:19 +03:00
|
|
|
// cleanup stops and resets all the modules.
|
|
|
|
func cleanup(ctx context.Context) {
|
2019-02-25 16:44:22 +03:00
|
|
|
log.Info("Stopping AdGuard Home")
|
2019-02-04 13:54:53 +03:00
|
|
|
|
2020-02-19 15:24:55 +03:00
|
|
|
if Context.web != nil {
|
2021-01-26 19:44:19 +03:00
|
|
|
Context.web.Close(ctx)
|
2020-02-19 15:24:55 +03:00
|
|
|
Context.web = nil
|
|
|
|
}
|
|
|
|
if Context.auth != nil {
|
|
|
|
Context.auth.Close()
|
|
|
|
Context.auth = nil
|
|
|
|
}
|
2020-02-18 19:27:09 +03:00
|
|
|
|
2018-12-05 15:36:18 +03:00
|
|
|
err := stopDNSServer()
|
|
|
|
if err != nil {
|
2019-02-25 16:44:22 +03:00
|
|
|
log.Error("Couldn't stop DNS server: %s", err)
|
2018-12-05 15:36:18 +03:00
|
|
|
}
|
2020-07-03 18:20:01 +03:00
|
|
|
|
|
|
|
if Context.dhcpServer != nil {
|
|
|
|
Context.dhcpServer.Stop()
|
2019-02-04 13:54:53 +03:00
|
|
|
}
|
2020-02-19 15:28:06 +03:00
|
|
|
|
2021-04-12 18:31:45 +03:00
|
|
|
Context.etcHosts.Close()
|
2020-03-20 15:05:43 +03:00
|
|
|
|
2020-02-19 15:28:06 +03:00
|
|
|
if Context.tls != nil {
|
|
|
|
Context.tls.Close()
|
|
|
|
Context.tls = nil
|
|
|
|
}
|
2018-12-05 15:36:18 +03:00
|
|
|
}
|
|
|
|
|
2019-04-05 12:19:28 +03:00
|
|
|
// This function is called before application exits
|
|
|
|
func cleanupAlways() {
|
2020-02-13 18:42:07 +03:00
|
|
|
if len(Context.pidFileName) != 0 {
|
|
|
|
_ = os.Remove(Context.pidFileName)
|
2019-04-05 12:19:28 +03:00
|
|
|
}
|
2019-05-08 10:43:47 +03:00
|
|
|
log.Info("Stopped")
|
2019-04-05 12:19:28 +03:00
|
|
|
}
|
|
|
|
|
2020-09-07 11:10:56 +03:00
|
|
|
func exitWithError() {
|
|
|
|
os.Exit(64)
|
2019-02-04 13:54:53 +03:00
|
|
|
}
|
|
|
|
|
2019-01-24 20:11:01 +03:00
|
|
|
// loadOptions reads command line arguments and initializes configuration
|
2019-02-04 13:54:53 +03:00
|
|
|
func loadOptions() options {
|
2020-09-07 11:10:56 +03:00
|
|
|
o, f, err := parse(os.Args[0], os.Args[1:])
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err.Error())
|
|
|
|
_ = printHelp(os.Args[0])
|
|
|
|
exitWithError()
|
|
|
|
} else if f != nil {
|
|
|
|
err = f()
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err.Error())
|
|
|
|
exitWithError()
|
|
|
|
} else {
|
2020-01-16 10:42:03 +03:00
|
|
|
os.Exit(0)
|
2019-01-24 20:11:01 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-04 13:54:53 +03:00
|
|
|
return o
|
2019-01-24 20:11:01 +03:00
|
|
|
}
|
2019-02-22 17:59:42 +03:00
|
|
|
|
2021-06-01 21:06:55 +03:00
|
|
|
// 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
|
2021-03-15 14:19:04 +03:00
|
|
|
// admin interface. proto is either schemeHTTP or schemeHTTPS.
|
2019-02-22 17:59:42 +03:00
|
|
|
func printHTTPAddresses(proto string) {
|
2020-02-19 15:28:06 +03:00
|
|
|
tlsConf := tlsConfigSettings{}
|
2020-04-07 19:24:29 +03:00
|
|
|
if Context.tls != nil {
|
|
|
|
Context.tls.WriteDiskConfig(&tlsConf)
|
|
|
|
}
|
2020-05-16 02:02:50 +03:00
|
|
|
|
2021-06-01 21:06:55 +03:00
|
|
|
port := config.BindPort
|
2021-03-15 14:19:04 +03:00
|
|
|
if proto == schemeHTTPS {
|
2021-06-01 21:06:55 +03:00
|
|
|
port = tlsConf.PortHTTPS
|
2020-05-16 02:02:50 +03:00
|
|
|
}
|
|
|
|
|
2021-06-01 21:06:55 +03:00
|
|
|
// TODO(e.burkov): Inspect and perhaps merge with the previous
|
|
|
|
// condition.
|
2021-03-15 14:19:04 +03:00
|
|
|
if proto == schemeHTTPS && tlsConf.ServerName != "" {
|
2021-06-01 21:06:55 +03:00
|
|
|
printWebAddrs(proto, tlsConf.ServerName, tlsConf.PortHTTPS, 0)
|
2019-02-22 17:59:42 +03:00
|
|
|
|
2021-06-01 21:06:55 +03:00
|
|
|
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)
|
2020-12-29 19:53:56 +03:00
|
|
|
}
|
2019-02-22 17:59:42 +03:00
|
|
|
}
|
|
|
|
}
|
2020-02-13 18:42:07 +03:00
|
|
|
|
|
|
|
// -------------------
|
|
|
|
// first run / install
|
|
|
|
// -------------------
|
|
|
|
func detectFirstRun() bool {
|
|
|
|
configfile := Context.configFilename
|
|
|
|
if !filepath.IsAbs(configfile) {
|
|
|
|
configfile = filepath.Join(Context.workDir, Context.configFilename)
|
|
|
|
}
|
|
|
|
_, err := os.Stat(configfile)
|
2021-01-27 18:32:13 +03:00
|
|
|
return errors.Is(err, os.ErrNotExist)
|
2020-02-13 18:42:07 +03:00
|
|
|
}
|
|
|
|
|
2021-04-22 13:38:24 +03:00
|
|
|
// Connect to a remote server resolving hostname using our own DNS server.
|
|
|
|
//
|
|
|
|
// TODO(e.burkov): This messy logic should be decomposed and clarified.
|
2021-03-12 14:32:08 +03:00
|
|
|
func customDialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) {
|
2020-02-13 18:42:07 +03:00
|
|
|
log.Tracef("network:%v addr:%v", network, addr)
|
|
|
|
|
|
|
|
host, port, err := net.SplitHostPort(addr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
dialer := &net.Dialer{
|
|
|
|
Timeout: time.Minute * 5,
|
|
|
|
}
|
|
|
|
|
|
|
|
if net.ParseIP(host) != nil || config.DNS.Port == 0 {
|
2021-03-12 14:32:08 +03:00
|
|
|
return dialer.DialContext(ctx, network, addr)
|
2020-02-13 18:42:07 +03:00
|
|
|
}
|
|
|
|
|
2021-03-12 14:32:08 +03:00
|
|
|
addrs, err := Context.dnsServer.Resolve(host)
|
2020-02-13 18:42:07 +03:00
|
|
|
log.Debug("dnsServer.Resolve: %s: %v", host, addrs)
|
2021-03-12 14:32:08 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2020-02-13 18:42:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(addrs) == 0 {
|
|
|
|
return nil, fmt.Errorf("couldn't lookup host: %s", host)
|
|
|
|
}
|
|
|
|
|
|
|
|
var dialErrs []error
|
|
|
|
for _, a := range addrs {
|
|
|
|
addr = net.JoinHostPort(a.String(), port)
|
2021-03-12 14:32:08 +03:00
|
|
|
conn, err = dialer.DialContext(ctx, network, addr)
|
2020-02-13 18:42:07 +03:00
|
|
|
if err != nil {
|
|
|
|
dialErrs = append(dialErrs, err)
|
2021-03-12 14:32:08 +03:00
|
|
|
|
2020-02-13 18:42:07 +03:00
|
|
|
continue
|
|
|
|
}
|
2021-03-12 14:32:08 +03:00
|
|
|
|
|
|
|
return conn, err
|
2020-02-13 18:42:07 +03:00
|
|
|
}
|
2021-03-12 14:32:08 +03:00
|
|
|
|
2021-05-24 17:28:11 +03:00
|
|
|
return nil, errors.List(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
|
2020-02-13 18:42:07 +03:00
|
|
|
}
|
2020-03-12 15:11:08 +03:00
|
|
|
|
2021-01-13 16:18:51 +03:00
|
|
|
func getHTTPProxy(_ *http.Request) (*url.URL, error) {
|
|
|
|
if config.ProxyURL == "" {
|
2020-03-12 15:11:08 +03:00
|
|
|
return nil, nil
|
|
|
|
}
|
2021-01-13 16:18:51 +03:00
|
|
|
|
2020-03-12 15:11:08 +03:00
|
|
|
return url.Parse(config.ProxyURL)
|
|
|
|
}
|
2020-11-25 18:09:41 +03:00
|
|
|
|
|
|
|
// jsonError is a generic JSON error response.
|
|
|
|
//
|
|
|
|
// TODO(a.garipov): Merge together with the implementations in .../dhcpd and
|
|
|
|
// other packages after refactoring the web handler registering.
|
|
|
|
type jsonError struct {
|
|
|
|
// Message is the error message, an opaque string.
|
|
|
|
Message string `json:"message"`
|
|
|
|
}
|