2018-08-30 14:25:33 +00:00
package main
import (
2019-02-12 18:14:02 +00:00
"crypto/tls"
2018-08-30 14:25:33 +00:00
"fmt"
2019-02-04 10:54:53 +00:00
stdlog "log"
2018-08-30 14:25:33 +00:00
"net"
"net/http"
"os"
2018-12-05 12:36:18 +00:00
"os/signal"
2018-08-30 14:25:33 +00:00
"path/filepath"
2019-02-05 11:09:05 +00:00
"runtime"
2018-08-30 14:25:33 +00:00
"strconv"
2019-02-12 18:14:02 +00:00
"sync"
2018-12-05 12:36:18 +00:00
"syscall"
2018-11-27 18:25:03 +00:00
"time"
2018-08-30 14:25:33 +00:00
"github.com/gobuffalo/packr"
2019-02-05 11:09:05 +00:00
2018-12-29 16:12:22 +00:00
"github.com/hmage/golibs/log"
2018-08-30 14:25:33 +00:00
)
// VersionString will be set through ldflags, contains current version
var VersionString = "undefined"
2019-02-06 13:47:17 +00:00
var httpServer * http . Server
2019-02-12 18:14:02 +00:00
var httpsServer struct {
server * http . Server
2019-02-14 15:00:23 +00:00
cond * sync . Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
2019-02-12 18:14:02 +00:00
sync . Mutex // protects config.TLS
}
2018-08-30 14:25:33 +00:00
2019-02-05 11:09:05 +00:00
const (
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
configSyslog = "syslog"
)
2019-02-04 10:54:53 +00:00
// main is the entry point
2018-08-30 14:25:33 +00:00
func main ( ) {
2019-02-04 10:54:53 +00: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 14:25:33 +00:00
2019-02-04 10:54:53 +00:00
if args . serviceControlAction != "" {
handleServiceControlAction ( args . serviceControlAction )
return
2018-10-29 23:17:24 +00:00
}
2018-10-15 13:13:03 +00:00
2019-02-04 10:54:53 +00:00
signalChannel := make ( chan os . Signal )
signal . Notify ( signalChannel , syscall . SIGINT , syscall . SIGTERM , syscall . SIGHUP , syscall . SIGQUIT )
go func ( ) {
<- signalChannel
cleanup ( )
os . Exit ( 0 )
} ( )
2019-02-05 11:09:05 +00:00
// run the protection
run ( args )
2019-02-04 10:54:53 +00:00
}
// run initializes configuration and runs the AdGuard Home
2019-02-05 11:09:05 +00:00
// run is a blocking method and it won't exit until the service is stopped!
2019-02-04 10:54:53 +00:00
func run ( args options ) {
2019-02-05 11:09:05 +00:00
// config file path can be overridden by command-line arguments:
2019-02-04 10:54:53 +00:00
if args . configFilename != "" {
config . ourConfigFilename = args . configFilename
}
2019-02-05 11:09:05 +00:00
// configure working dir and config path
2019-02-05 17:35:48 +00:00
initWorkingDir ( args )
2019-02-05 11:09:05 +00:00
2019-02-04 10:54:53 +00:00
// configure log level and output
configureLogger ( args )
// print the first message after logger is configured
log . Printf ( "AdGuard Home, version %s\n" , VersionString )
2019-02-10 17:47:43 +00:00
log . Tracef ( "Current working directory is %s" , config . ourWorkingDir )
2019-02-05 11:09:05 +00:00
if args . runningAsService {
log . Printf ( "AdGuard Home is running as a service" )
}
2019-02-04 10:54:53 +00:00
2019-01-29 17:41:57 +00:00
config . firstRun = detectFirstRun ( )
2019-02-04 10:54:53 +00:00
// Do the upgrade if necessary
2019-01-29 17:41:57 +00:00
err := upgradeConfig ( )
2019-02-04 10:54:53 +00:00
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
}
2018-08-30 14:25:33 +00:00
2018-11-27 18:25:03 +00:00
// 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 ( )
}
2019-02-04 10:54:53 +00:00
err = filter . load ( )
2018-11-27 18:25:03 +00:00
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
}
2019-02-10 18:44:16 +00:00
2018-11-28 13:05:24 +00:00
if len ( filter . Rules ) == 0 {
2018-11-27 18:25:03 +00:00
filter . LastUpdated = time . Time { }
}
}
2018-10-29 23:17:24 +00:00
// Save the updated config
2019-02-04 10:54:53 +00:00
err = config . write ( )
2018-08-30 14:25:33 +00:00
if err != nil {
log . Fatal ( err )
}
2019-02-10 17:47:43 +00:00
// Init the DNS server instance before registering HTTP handlers
dnsBaseDir := filepath . Join ( config . ourWorkingDir , dataDir )
initDNSServer ( dnsBaseDir )
2019-02-01 16:25:04 +00:00
if ! config . firstRun {
err = startDNSServer ( )
if err != nil {
log . Fatal ( err )
}
2018-09-05 23:00:57 +00:00
2019-02-01 16:25:04 +00:00
err = startDHCPServer ( )
if err != nil {
log . Fatal ( err )
}
2018-12-28 18:01:16 +00:00
}
2019-02-04 10:54:53 +00:00
// 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 ( )
2019-02-05 11:09:05 +00:00
// Initialize and run the admin Web interface
box := packr . NewBox ( "build/static" )
2019-01-29 17:41:57 +00:00
// if not configured, redirect / to /install.html, otherwise redirect /install.html to /
http . Handle ( "/" , postInstallHandler ( optionalAuthHandler ( http . FileServer ( box ) ) ) )
2019-02-05 11:09:05 +00:00
registerControlHandlers ( )
2019-02-06 13:48:04 +00:00
// add handlers for /install paths, we only need them when we're not configured yet
if config . firstRun {
2019-02-07 11:22:08 +00:00
log . Printf ( "This is the first launch of AdGuard Home, redirecting everything to /install.html " )
2019-02-06 13:48:04 +00:00
http . Handle ( "/install.html" , preInstallHandler ( http . FileServer ( box ) ) )
registerInstallHandlers ( )
}
2019-02-12 18:14:02 +00:00
httpsServer . cond = sync . NewCond ( & httpsServer . Mutex )
// for https, we have a separate goroutine loop
go func ( ) {
for { // this is an endless loop
httpsServer . cond . L . Lock ( )
// this mechanism doesn't let us through until all conditions are ment
2019-02-21 14:33:46 +00:00
for config . TLS . Enabled == false || config . TLS . PortHTTPS == 0 || config . TLS . PrivateKey == "" || config . TLS . CertificateChain == "" { // sleep until necessary data is supplied
2019-02-12 18:14:02 +00:00
httpsServer . cond . Wait ( )
}
address := net . JoinHostPort ( config . BindHost , strconv . Itoa ( config . TLS . PortHTTPS ) )
2019-02-13 08:45:23 +00:00
// validate current TLS config and update warnings (it could have been loaded from file)
2019-02-27 11:11:41 +00:00
data := validateCertificates ( config . TLS . CertificateChain , config . TLS . PrivateKey , config . TLS . ServerName )
2019-02-27 14:36:02 +00:00
if ! data . ValidPair {
2019-02-15 14:06:55 +00:00
log . Fatal ( data . WarningValidation )
2019-02-12 18:14:02 +00:00
os . Exit ( 1 )
}
2019-02-21 16:07:12 +00:00
config . Lock ( )
2019-02-27 11:11:41 +00:00
config . TLS . tlsConfigStatus = data // update warnings
2019-02-21 16:07:12 +00:00
config . Unlock ( )
2019-02-13 08:45:23 +00:00
2019-02-19 14:52:19 +00:00
// prepare certs for HTTPS server
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
certchain := make ( [ ] byte , len ( config . TLS . CertificateChain ) )
copy ( certchain , [ ] byte ( config . TLS . CertificateChain ) )
privatekey := make ( [ ] byte , len ( config . TLS . PrivateKey ) )
copy ( privatekey , [ ] byte ( config . TLS . PrivateKey ) )
cert , err := tls . X509KeyPair ( certchain , privatekey )
2019-02-13 08:45:23 +00:00
if err != nil {
log . Fatal ( err )
os . Exit ( 1 )
2019-02-12 18:14:02 +00:00
}
2019-02-13 08:45:23 +00:00
httpsServer . cond . L . Unlock ( )
// prepare HTTPS server
2019-02-12 18:14:02 +00:00
httpsServer . server = & http . Server {
2019-02-13 08:45:23 +00:00
Addr : address ,
TLSConfig : & tls . Config {
Certificates : [ ] tls . Certificate { cert } ,
} ,
2019-02-12 18:14:02 +00:00
}
2019-02-22 14:59:42 +00:00
printHTTPAddresses ( "https" )
2019-02-12 18:14:02 +00:00
err = httpsServer . server . ListenAndServeTLS ( "" , "" )
if err != http . ErrServerClosed {
log . Fatal ( err )
os . Exit ( 1 )
}
}
} ( )
2019-02-06 13:47:17 +00:00
// this loop is used as an ability to change listening host and/or port
for {
2019-02-22 14:59:42 +00:00
printHTTPAddresses ( "http" )
2019-02-06 13:47:17 +00:00
// we need to have new instance, because after Shutdown() the Server is not usable
2019-02-22 14:59:42 +00:00
address := net . JoinHostPort ( config . BindHost , strconv . Itoa ( config . BindPort ) )
2019-02-06 13:47:17 +00:00
httpServer = & http . Server {
Addr : address ,
}
err := httpServer . ListenAndServe ( )
if err != http . ErrServerClosed {
log . Fatal ( err )
os . Exit ( 1 )
}
// We use ErrServerClosed as a sign that we need to rebind on new address, so go back to the start of the loop
}
2019-02-05 11:09:05 +00:00
}
2019-02-10 17:47:43 +00:00
// initWorkingDir initializes the ourWorkingDir
// if no command-line arguments specified, we use the directory where our binary file is located
2019-02-05 17:35:48 +00:00
func initWorkingDir ( args options ) {
2019-02-05 11:09:05 +00:00
exec , err := os . Executable ( )
if err != nil {
panic ( err )
}
2019-02-10 17:47:43 +00:00
if args . workDir != "" {
2019-02-05 17:35:48 +00:00
// If there is a custom config file, use it's directory as our working dir
2019-02-10 17:47:43 +00:00
config . ourWorkingDir = args . workDir
2019-02-05 11:09:05 +00:00
} else {
2019-02-10 17:47:43 +00:00
config . ourWorkingDir = filepath . Dir ( exec )
2019-02-05 11:09:05 +00:00
}
2019-02-04 10:54:53 +00:00
}
// 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
2019-02-05 11:09:05 +00: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
}
2019-02-04 10:54:53 +00:00
if ls . LogFile == "" {
return
}
2019-02-05 11:09:05 +00:00
if ls . LogFile == configSyslog {
// Use syslog where it is possible and eventlog on Windows
err := configureSyslog ( )
2019-02-04 10:54:53 +00:00
if err != nil {
log . Fatalf ( "cannot initialize syslog: %s" , err )
}
2019-02-05 11:09:05 +00:00
} else {
2019-02-10 17:47:43 +00:00
logFilePath := filepath . Join ( config . ourWorkingDir , ls . LogFile )
2019-02-05 11:09:05 +00:00
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 )
2019-02-04 10:54:53 +00:00
}
2018-08-30 14:25:33 +00:00
}
2018-10-12 16:40:43 +00:00
2018-12-05 12:36:18 +00:00
func cleanup ( ) {
2019-02-04 10:54:53 +00:00
log . Printf ( "Stopping AdGuard Home" )
2018-12-05 12:36:18 +00:00
err := stopDNSServer ( )
if err != nil {
log . Printf ( "Couldn't stop DNS server: %s" , err )
}
2019-02-04 10:54:53 +00:00
err = stopDHCPServer ( )
if err != nil {
log . Printf ( "Couldn't stop DHCP server: %s" , err )
}
2018-12-05 12:36:18 +00:00
}
2019-02-04 10:54:53 +00:00
// command-line arguments
type options struct {
verbose bool // is verbose logging enabled
configFilename string // path to the config file
2019-02-10 17:47:43 +00:00
workDir string // path to the working directory where we will store the filters data and the querylog
2019-02-04 10:54:53 +00:00
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
2019-02-05 11:09:05 +00:00
// runningAsService flag is set to true when options are passed from the service runner
runningAsService bool
2019-02-04 10:54:53 +00:00
}
2019-01-24 17:11:01 +00:00
// loadOptions reads command line arguments and initializes configuration
2019-02-04 10:54:53 +00:00
func loadOptions ( ) options {
o := options { }
2019-01-24 17:11:01 +00:00
var printHelp func ( )
var opts = [ ] struct {
longName string
shortName string
description string
callbackWithValue func ( value string )
callbackNoValue func ( )
} {
2019-02-10 17:47:43 +00:00
{ "config" , "c" , "path to the config file" , func ( value string ) { o . configFilename = value } , nil } ,
{ "work-dir" , "w" , "path to the working directory" , func ( value string ) { o . workDir = value } , nil } ,
2019-02-05 20:29:11 +00:00
{ "host" , "h" , "host address to bind HTTP server on" , func ( value string ) { o . bindHost = value } , nil } ,
2019-01-24 17:11:01 +00:00
{ "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" )
}
2019-02-04 10:54:53 +00:00
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
2019-01-24 17:11:01 +00:00
} , nil } ,
2019-02-04 10:54:53 +00:00
{ "verbose" , "v" , "enable verbose output" , nil , func ( ) { o . verbose = true } } ,
2019-02-05 20:29:11 +00:00
{ "help" , "" , "print this help" , nil , func ( ) {
2019-02-04 10:54:53 +00:00
printHelp ( )
os . Exit ( 64 )
} } ,
2019-01-24 17:11:01 +00:00
}
printHelp = func ( ) {
fmt . Printf ( "Usage:\n\n" )
fmt . Printf ( "%s [options]\n\n" , os . Args [ 0 ] )
fmt . Printf ( "Options:\n" )
for _ , opt := range opts {
2019-02-05 20:29:11 +00:00
if opt . shortName != "" {
fmt . Printf ( " -%s, %-30s %s\n" , opt . shortName , "--" + opt . longName , opt . description )
} else {
fmt . Printf ( " %-34s %s\n" , "--" + opt . longName , opt . description )
}
2019-01-24 17:11:01 +00:00
}
}
for i := 1 ; i < len ( os . Args ) ; i ++ {
v := os . Args [ i ]
knownParam := false
for _ , opt := range opts {
2019-02-05 20:29:11 +00:00
if v == "--" + opt . longName || ( opt . shortName != "" && v == "-" + opt . shortName ) {
2019-01-24 17:11:01 +00:00
if opt . callbackWithValue != nil {
2019-02-04 10:54:53 +00:00
if i + 1 >= len ( os . Args ) {
2019-01-24 17:11:01 +00:00
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 )
}
}
2019-02-04 10:54:53 +00:00
return o
2019-01-24 17:11:01 +00:00
}
2019-02-22 14:59:42 +00:00
// prints IP addresses which user can use to open the admin interface
// proto is either "http" or "https"
func printHTTPAddresses ( proto string ) {
var address string
2019-02-22 15:47:54 +00:00
if proto == "https" && config . TLS . ServerName != "" {
if config . TLS . PortHTTPS == 443 {
log . Printf ( "Go to https://%s" , config . TLS . ServerName )
} else {
log . Printf ( "Go to https://%s:%d" , config . TLS . ServerName , config . TLS . PortHTTPS )
}
} else if config . BindHost == "0.0.0.0" {
2019-02-22 14:59:42 +00:00
log . Println ( "AdGuard Home is available on the following addresses:" )
ifaces , err := getValidNetInterfacesForWeb ( )
if err != nil {
// That's weird, but we'll ignore it
address = net . JoinHostPort ( config . BindHost , strconv . Itoa ( config . BindPort ) )
log . Printf ( "Go to %s://%s" , proto , address )
return
}
for _ , iface := range ifaces {
address = net . JoinHostPort ( iface . Addresses [ 0 ] , strconv . Itoa ( config . BindPort ) )
log . Printf ( "Go to %s://%s" , proto , address )
}
} else {
address = net . JoinHostPort ( config . BindHost , strconv . Itoa ( config . BindPort ) )
log . Printf ( "Go to %s://%s" , proto , address )
}
}