405 lines
10 KiB
Go
405 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
stdlog "log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gobuffalo/packr"
|
|
|
|
"github.com/hmage/golibs/log"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
// VersionString will be set through ldflags, contains current version
|
|
var VersionString = "undefined"
|
|
|
|
const (
|
|
// We use it to detect the working dir
|
|
executableName = "AdGuardHome"
|
|
|
|
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
|
|
configSyslog = "syslog"
|
|
)
|
|
|
|
// main is the entry point
|
|
func main() {
|
|
// 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()
|
|
|
|
if args.serviceControlAction != "" {
|
|
handleServiceControlAction(args.serviceControlAction)
|
|
return
|
|
}
|
|
|
|
signalChannel := make(chan os.Signal)
|
|
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
|
go func() {
|
|
<-signalChannel
|
|
cleanup()
|
|
os.Exit(0)
|
|
}()
|
|
|
|
// run the protection
|
|
run(args)
|
|
}
|
|
|
|
// run initializes configuration and runs the AdGuard Home
|
|
// run is a blocking method and it won't exit until the service is stopped!
|
|
func run(args options) {
|
|
// config file path can be overridden by command-line arguments:
|
|
if args.configFilename != "" {
|
|
config.ourConfigFilename = args.configFilename
|
|
}
|
|
|
|
// configure working dir and config path
|
|
initWorkingDir()
|
|
|
|
// configure log level and output
|
|
configureLogger(args)
|
|
|
|
// print the first message after logger is configured
|
|
log.Printf("AdGuard Home, version %s\n", VersionString)
|
|
log.Printf("Current working directory is %s", config.ourBinaryDir)
|
|
if args.runningAsService {
|
|
log.Printf("AdGuard Home is running as a service")
|
|
}
|
|
|
|
err := askUsernamePasswordIfPossible()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Do the upgrade if necessary
|
|
err = upgradeConfig()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// parse from config file
|
|
err = parseConfig()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// override bind host/port from the console
|
|
if args.bindHost != "" {
|
|
config.BindHost = args.bindHost
|
|
}
|
|
if args.bindPort != 0 {
|
|
config.BindPort = args.bindPort
|
|
}
|
|
|
|
// Load filters from the disk
|
|
// And if any filter has zero ID, assign a new one
|
|
for i := range config.Filters {
|
|
filter := &config.Filters[i] // otherwise we're operating on a copy
|
|
if filter.ID == 0 {
|
|
filter.ID = assignUniqueFilterID()
|
|
}
|
|
err = filter.load()
|
|
if err != nil {
|
|
// This is okay for the first start, the filter will be loaded later
|
|
log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err)
|
|
// clear LastUpdated so it gets fetched right away
|
|
}
|
|
if len(filter.Rules) == 0 {
|
|
filter.LastUpdated = time.Time{}
|
|
}
|
|
}
|
|
|
|
// Save the updated config
|
|
err = config.write()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
err = startDNSServer()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
err = startDHCPServer()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Update filters we've just loaded right away, don't wait for periodic update timer
|
|
go func() {
|
|
refreshFiltersIfNecessary(false)
|
|
// Save the updated config
|
|
err := config.write()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
// Schedule automatic filters updates
|
|
go periodicallyRefreshFilters()
|
|
|
|
// Initialize and run the admin Web interface
|
|
box := packr.NewBox("build/static")
|
|
http.Handle("/", optionalAuthHandler(http.FileServer(box)))
|
|
registerControlHandlers()
|
|
|
|
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
|
|
URL := fmt.Sprintf("http://%s", address)
|
|
log.Println("Go to " + URL)
|
|
log.Fatal(http.ListenAndServe(address, nil))
|
|
}
|
|
|
|
// initWorkingDir initializes the ourBinaryDir (basically, we use it as a working dir)
|
|
func initWorkingDir() {
|
|
exec, err := os.Executable()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
currentExecutableName := filepath.Base(exec)
|
|
currentExecutableName = strings.TrimSuffix(currentExecutableName, path.Ext(currentExecutableName))
|
|
if currentExecutableName == executableName {
|
|
// Binary build
|
|
config.ourBinaryDir = filepath.Dir(exec)
|
|
} else {
|
|
// Most likely we're debugging -- using current working directory in this case
|
|
workDir, _ := os.Getwd()
|
|
config.ourBinaryDir = workDir
|
|
}
|
|
}
|
|
|
|
// configureLogger configures logger level and output
|
|
func configureLogger(args options) {
|
|
ls := getLogSettings()
|
|
|
|
// command-line arguments can override config settings
|
|
if args.verbose {
|
|
ls.Verbose = true
|
|
}
|
|
if args.logFile != "" {
|
|
ls.LogFile = args.logFile
|
|
}
|
|
|
|
log.Verbose = ls.Verbose
|
|
|
|
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
|
|
}
|
|
|
|
if ls.LogFile == "" {
|
|
return
|
|
}
|
|
|
|
if ls.LogFile == configSyslog {
|
|
// Use syslog where it is possible and eventlog on Windows
|
|
err := configureSyslog()
|
|
if err != nil {
|
|
log.Fatalf("cannot initialize syslog: %s", err)
|
|
}
|
|
} else {
|
|
logFilePath := filepath.Join(config.ourBinaryDir, ls.LogFile)
|
|
file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755)
|
|
if err != nil {
|
|
log.Fatalf("cannot create a log file: %s", err)
|
|
}
|
|
stdlog.SetOutput(file)
|
|
}
|
|
}
|
|
|
|
func cleanup() {
|
|
log.Printf("Stopping AdGuard Home")
|
|
|
|
err := stopDNSServer()
|
|
if err != nil {
|
|
log.Printf("Couldn't stop DNS server: %s", err)
|
|
}
|
|
err = stopDHCPServer()
|
|
if err != nil {
|
|
log.Printf("Couldn't stop DHCP server: %s", err)
|
|
}
|
|
}
|
|
|
|
func getInput() (string, error) {
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
scanner.Scan()
|
|
text := scanner.Text()
|
|
err := scanner.Err()
|
|
return text, err
|
|
}
|
|
|
|
// command-line arguments
|
|
type options struct {
|
|
verbose bool // is verbose logging enabled
|
|
configFilename string // path to the config file
|
|
bindHost string // host address to bind HTTP server on
|
|
bindPort int // port to serve HTTP pages on
|
|
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
|
|
|
// service control action (see service.ControlAction array + "status" command)
|
|
serviceControlAction string
|
|
|
|
// runningAsService flag is set to true when options are passed from the service runner
|
|
runningAsService bool
|
|
}
|
|
|
|
// loadOptions reads command line arguments and initializes configuration
|
|
func loadOptions() options {
|
|
o := options{}
|
|
|
|
var printHelp func()
|
|
var opts = []struct {
|
|
longName string
|
|
shortName string
|
|
description string
|
|
callbackWithValue func(value string)
|
|
callbackNoValue func()
|
|
}{
|
|
{"config", "c", "path to config file", func(value string) { o.configFilename = value }, nil},
|
|
{"host", "o", "host address to bind HTTP server on", func(value string) { o.bindHost = value }, nil},
|
|
{"port", "p", "port to serve HTTP pages on", func(value string) {
|
|
v, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
panic("Got port that is not a number")
|
|
}
|
|
o.bindPort = v
|
|
}, nil},
|
|
{"service", "s", "service control action: status, install, uninstall, start, stop, restart", func(value string) {
|
|
o.serviceControlAction = value
|
|
}, nil},
|
|
{"logfile", "l", "path to the log file. If empty, writes to stdout, if 'syslog' -- system log", func(value string) {
|
|
o.logFile = value
|
|
}, nil},
|
|
{"verbose", "v", "enable verbose output", nil, func() { o.verbose = true }},
|
|
{"help", "h", "print this help", nil, func() {
|
|
printHelp()
|
|
os.Exit(64)
|
|
}},
|
|
}
|
|
printHelp = func() {
|
|
fmt.Printf("Usage:\n\n")
|
|
fmt.Printf("%s [options]\n\n", os.Args[0])
|
|
fmt.Printf("Options:\n")
|
|
for _, opt := range opts {
|
|
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description)
|
|
}
|
|
}
|
|
for i := 1; i < len(os.Args); i++ {
|
|
v := os.Args[i]
|
|
knownParam := false
|
|
for _, opt := range opts {
|
|
if v == "--"+opt.longName || v == "-"+opt.shortName {
|
|
if opt.callbackWithValue != nil {
|
|
if i+1 >= len(os.Args) {
|
|
log.Printf("ERROR: Got %s without argument\n", v)
|
|
os.Exit(64)
|
|
}
|
|
i++
|
|
opt.callbackWithValue(os.Args[i])
|
|
} else if opt.callbackNoValue != nil {
|
|
opt.callbackNoValue()
|
|
}
|
|
knownParam = true
|
|
break
|
|
}
|
|
}
|
|
if !knownParam {
|
|
log.Printf("ERROR: unknown option %v\n", v)
|
|
printHelp()
|
|
os.Exit(64)
|
|
}
|
|
}
|
|
|
|
return o
|
|
}
|
|
|
|
func promptAndGet(prompt string) (string, error) {
|
|
for {
|
|
fmt.Print(prompt)
|
|
input, err := getInput()
|
|
if err != nil {
|
|
log.Printf("Failed to get input, aborting: %s", err)
|
|
return "", err
|
|
}
|
|
if len(input) != 0 {
|
|
return input, nil
|
|
}
|
|
// try again
|
|
}
|
|
}
|
|
|
|
func promptAndGetPassword(prompt string) (string, error) {
|
|
for {
|
|
fmt.Print(prompt)
|
|
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
|
fmt.Print("\n")
|
|
if err != nil {
|
|
log.Printf("Failed to get input, aborting: %s", err)
|
|
return "", err
|
|
}
|
|
if len(password) != 0 {
|
|
return string(password), nil
|
|
}
|
|
// try again
|
|
}
|
|
}
|
|
|
|
func askUsernamePasswordIfPossible() error {
|
|
configfile := config.ourConfigFilename
|
|
if !filepath.IsAbs(configfile) {
|
|
configfile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
|
}
|
|
_, err := os.Stat(configfile)
|
|
if !os.IsNotExist(err) {
|
|
// do nothing, file exists
|
|
return nil
|
|
}
|
|
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
|
return nil // do nothing
|
|
}
|
|
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
|
|
return nil // do nothing
|
|
}
|
|
fmt.Printf("Would you like to set user/password for the web interface authentication (yes/no)?\n")
|
|
yesno, err := promptAndGet("Please type 'yes' or 'no': ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if yesno[0] != 'y' && yesno[0] != 'Y' {
|
|
return nil
|
|
}
|
|
username, err := promptAndGet("Please enter the username: ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
password, err := promptAndGetPassword("Please enter the password: ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
password2, err := promptAndGetPassword("Please enter password again: ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if password2 != password {
|
|
fmt.Printf("Passwords do not match! Aborting\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
config.AuthName = username
|
|
config.AuthPass = password
|
|
return nil
|
|
}
|