e8129f15c7
1. Auth module was initialized inside dns.go - now it's moved to initWeb() 2. stopHTTPServer() wasn't called on server stop - now we do that 3. Don't use postInstall() HTTP filter where it's not necessary. Now we register handlers after installation is complete.
726 lines
20 KiB
Go
726 lines
20 KiB
Go
package home
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/util"
|
|
|
|
"github.com/joomcode/errorx"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/isdelve"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
|
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
|
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
|
"github.com/AdguardTeam/AdGuardHome/querylog"
|
|
"github.com/AdguardTeam/AdGuardHome/stats"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/NYTimes/gziphandler"
|
|
"github.com/gobuffalo/packr"
|
|
)
|
|
|
|
const (
|
|
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
|
|
configSyslog = "syslog"
|
|
)
|
|
|
|
// Update-related variables
|
|
var (
|
|
versionString string
|
|
updateChannel string
|
|
versionCheckURL string
|
|
ARMVersion string
|
|
)
|
|
|
|
const versionCheckPeriod = time.Hour * 8
|
|
|
|
// Global context
|
|
type homeContext struct {
|
|
// Modules
|
|
// --
|
|
|
|
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
|
|
dnsFilter *dnsfilter.Dnsfilter // DNS filtering module
|
|
dhcpServer *dhcpd.Server // DHCP module
|
|
auth *Auth // HTTP authentication module
|
|
httpServer *http.Server // HTTP module
|
|
httpsServer HTTPSServer // HTTPS module
|
|
filters Filtering
|
|
|
|
// 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
|
|
tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
|
|
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)
|
|
}
|
|
|
|
// Context - a global context object
|
|
var Context homeContext
|
|
|
|
// Main is the entry point
|
|
func Main(version string, channel string, armVer string) {
|
|
// Init update-related global variables
|
|
versionString = version
|
|
updateChannel = channel
|
|
ARMVersion = armVer
|
|
versionCheckURL = "https://static.adguard.com/adguardhome/" + updateChannel + "/version.json"
|
|
|
|
// 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
|
|
}
|
|
|
|
Context.appSignalChannel = make(chan os.Signal)
|
|
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
|
go func() {
|
|
for {
|
|
sig := <-Context.appSignalChannel
|
|
log.Info("Received signal '%s'", sig)
|
|
switch sig {
|
|
case syscall.SIGHUP:
|
|
Context.clients.Reload()
|
|
|
|
default:
|
|
cleanup()
|
|
cleanupAlways()
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// run the protection
|
|
run(args)
|
|
}
|
|
|
|
// run initializes configuration and runs the AdGuard Home
|
|
// run is a blocking method!
|
|
// nolint
|
|
func run(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"
|
|
}
|
|
|
|
// configure working dir and config path
|
|
initWorkingDir(args)
|
|
|
|
// configure log level and output
|
|
configureLogger(args)
|
|
|
|
// print the first message after logger is configured
|
|
msg := "AdGuard Home, version %s, channel %s\n, arch %s %s"
|
|
if ARMVersion != "" {
|
|
msg = msg + " v" + ARMVersion
|
|
}
|
|
log.Printf(msg, versionString, updateChannel, runtime.GOOS, runtime.GOARCH, ARMVersion)
|
|
log.Debug("Current working directory is %s", Context.workDir)
|
|
if args.runningAsService {
|
|
log.Info("AdGuard Home is running as a service")
|
|
}
|
|
Context.runningAsService = args.runningAsService
|
|
Context.disableUpdate = args.disableUpdate
|
|
|
|
Context.firstRun = detectFirstRun()
|
|
if Context.firstRun {
|
|
requireAdminRights()
|
|
}
|
|
|
|
initConfig()
|
|
initServices()
|
|
|
|
Context.tlsRoots = util.LoadSystemRootCAs()
|
|
Context.transport = &http.Transport{
|
|
DialContext: customDialContext,
|
|
Proxy: getHTTPProxy,
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: Context.tlsRoots,
|
|
},
|
|
}
|
|
Context.client = &http.Client{
|
|
Timeout: time.Minute * 5,
|
|
Transport: Context.transport,
|
|
}
|
|
|
|
if !Context.firstRun {
|
|
// Do the upgrade if necessary
|
|
err := upgradeConfig()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
err = parseConfig()
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
|
|
if args.checkConfig {
|
|
log.Info("Configuration file is OK")
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
config.DHCP.WorkDir = Context.workDir
|
|
config.DHCP.HTTPRegister = httpRegister
|
|
config.DHCP.ConfigModified = onConfigModified
|
|
Context.dhcpServer = dhcpd.Create(config.DHCP)
|
|
if Context.dhcpServer == nil {
|
|
os.Exit(1)
|
|
}
|
|
Context.clients.Init(config.Clients, Context.dhcpServer)
|
|
config.Clients = nil
|
|
|
|
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
|
config.RlimitNoFile != 0 {
|
|
util.SetRlimit(config.RlimitNoFile)
|
|
}
|
|
|
|
// override bind host/port from the console
|
|
if args.bindHost != "" {
|
|
config.BindHost = args.bindHost
|
|
}
|
|
if args.bindPort != 0 {
|
|
config.BindPort = args.bindPort
|
|
}
|
|
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
|
|
Context.pidFileName = args.pidFile
|
|
}
|
|
|
|
if !Context.firstRun {
|
|
// Save the updated config
|
|
err := config.write()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
err := os.MkdirAll(Context.getDataDir(), 0755)
|
|
if err != nil {
|
|
log.Fatalf("Cannot create DNS data dir at %s: %s", Context.getDataDir(), err)
|
|
}
|
|
|
|
err = initWeb()
|
|
if err != nil {
|
|
log.Fatalf("%s", err)
|
|
}
|
|
|
|
if !Context.firstRun {
|
|
err := initDNSServer()
|
|
if err != nil {
|
|
log.Fatalf("%s", err)
|
|
}
|
|
go func() {
|
|
err := startDNSServer()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
err = startDHCPServer()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
startWeb()
|
|
|
|
// wait indefinitely for other go-routines to complete their job
|
|
select {}
|
|
}
|
|
|
|
// Initialize Web modules
|
|
func initWeb() error {
|
|
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
|
Context.auth = InitAuth(sessFilename, config.Users, config.WebSessionTTLHours*60*60)
|
|
if Context.auth == nil {
|
|
return fmt.Errorf("Couldn't initialize Auth module")
|
|
}
|
|
config.Users = nil
|
|
|
|
// Initialize and run the admin Web interface
|
|
box := packr.NewBox("../build/static")
|
|
|
|
// if not configured, redirect / to /install.html, otherwise redirect /install.html to /
|
|
http.Handle("/", postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(http.FileServer(box)))))
|
|
|
|
// add handlers for /install paths, we only need them when we're not configured yet
|
|
if Context.firstRun {
|
|
log.Info("This is the first launch of AdGuard Home, redirecting everything to /install.html ")
|
|
http.Handle("/install.html", preInstallHandler(http.FileServer(box)))
|
|
registerInstallHandlers()
|
|
} else {
|
|
registerControlHandlers()
|
|
}
|
|
|
|
Context.httpsServer.cond = sync.NewCond(&Context.httpsServer.Mutex)
|
|
return nil
|
|
}
|
|
|
|
func startWeb() {
|
|
// for https, we have a separate goroutine loop
|
|
go httpServerLoop()
|
|
|
|
// this loop is used as an ability to change listening host and/or port
|
|
for !Context.httpsServer.shutdown {
|
|
printHTTPAddresses("http")
|
|
|
|
// we need to have new instance, because after Shutdown() the Server is not usable
|
|
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
|
|
Context.httpServer = &http.Server{
|
|
Addr: address,
|
|
}
|
|
err := Context.httpServer.ListenAndServe()
|
|
if err != http.ErrServerClosed {
|
|
cleanupAlways()
|
|
log.Fatal(err)
|
|
}
|
|
// We use ErrServerClosed as a sign that we need to rebind on new address, so go back to the start of the loop
|
|
}
|
|
}
|
|
|
|
func httpServerLoop() {
|
|
for !Context.httpsServer.shutdown {
|
|
Context.httpsServer.cond.L.Lock()
|
|
// this mechanism doesn't let us through until all conditions are met
|
|
for config.TLS.Enabled == false ||
|
|
config.TLS.PortHTTPS == 0 ||
|
|
len(config.TLS.PrivateKeyData) == 0 ||
|
|
len(config.TLS.CertificateChainData) == 0 { // sleep until necessary data is supplied
|
|
Context.httpsServer.cond.Wait()
|
|
}
|
|
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
|
|
// validate current TLS config and update warnings (it could have been loaded from file)
|
|
data := validateCertificates(string(config.TLS.CertificateChainData), string(config.TLS.PrivateKeyData), config.TLS.ServerName)
|
|
if !data.ValidPair {
|
|
cleanupAlways()
|
|
log.Fatal(data.WarningValidation)
|
|
}
|
|
config.Lock()
|
|
config.TLS.tlsConfigStatus = data // update warnings
|
|
config.Unlock()
|
|
|
|
// 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.CertificateChainData))
|
|
copy(certchain, config.TLS.CertificateChainData)
|
|
privatekey := make([]byte, len(config.TLS.PrivateKeyData))
|
|
copy(privatekey, config.TLS.PrivateKeyData)
|
|
cert, err := tls.X509KeyPair(certchain, privatekey)
|
|
if err != nil {
|
|
cleanupAlways()
|
|
log.Fatal(err)
|
|
}
|
|
Context.httpsServer.cond.L.Unlock()
|
|
|
|
// prepare HTTPS server
|
|
Context.httpsServer.server = &http.Server{
|
|
Addr: address,
|
|
TLSConfig: &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
MinVersion: tls.VersionTLS12,
|
|
RootCAs: Context.tlsRoots,
|
|
},
|
|
}
|
|
|
|
printHTTPAddresses("https")
|
|
err = Context.httpsServer.server.ListenAndServeTLS("", "")
|
|
if err != http.ErrServerClosed {
|
|
cleanupAlways()
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if the current user has root (administrator) rights
|
|
// and if not, ask and try to run as root
|
|
func requireAdminRights() {
|
|
admin, _ := util.HaveAdminRights()
|
|
if //noinspection ALL
|
|
admin || isdelve.Enabled {
|
|
return
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
log.Fatal("This is the first launch of AdGuard Home. You must run it as Administrator.")
|
|
|
|
} else {
|
|
log.Error("This is the first launch of AdGuard Home. You must run it as root.")
|
|
|
|
_, _ = io.WriteString(os.Stdout, "Do you want to start AdGuard Home as root user? [y/n] ")
|
|
stdin := bufio.NewReader(os.Stdin)
|
|
buf, _ := stdin.ReadString('\n')
|
|
buf = strings.TrimSpace(buf)
|
|
if buf != "y" {
|
|
os.Exit(1)
|
|
}
|
|
|
|
cmd := exec.Command("sudo", os.Args...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
_ = cmd.Run()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Write PID to a file
|
|
func writePIDFile(fn string) bool {
|
|
data := fmt.Sprintf("%d", os.Getpid())
|
|
err := ioutil.WriteFile(fn, []byte(data), 0644)
|
|
if err != nil {
|
|
log.Error("Couldn't write PID to file %s: %v", fn, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// initWorkingDir initializes the workDir
|
|
// if no command-line arguments specified, we use the directory where our binary file is located
|
|
func initWorkingDir(args options) {
|
|
execPath, err := os.Executable()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if args.workDir != "" {
|
|
// If there is a custom config file, use it's directory as our working dir
|
|
Context.workDir = args.workDir
|
|
} else {
|
|
Context.workDir = filepath.Dir(execPath)
|
|
}
|
|
}
|
|
|
|
// 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.SetLevel(log.INFO) - default
|
|
if ls.Verbose {
|
|
log.SetLevel(log.DEBUG)
|
|
}
|
|
|
|
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 := util.ConfigureSyslog(serviceName)
|
|
if err != nil {
|
|
log.Fatalf("cannot initialize syslog: %s", err)
|
|
}
|
|
} else {
|
|
logFilePath := filepath.Join(Context.workDir, ls.LogFile)
|
|
if filepath.IsAbs(ls.LogFile) {
|
|
logFilePath = ls.LogFile
|
|
}
|
|
|
|
file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
log.Fatalf("cannot create a log file: %s", err)
|
|
}
|
|
log.SetOutput(file)
|
|
}
|
|
}
|
|
|
|
func cleanup() {
|
|
log.Info("Stopping AdGuard Home")
|
|
|
|
stopHTTPServer()
|
|
|
|
err := stopDNSServer()
|
|
if err != nil {
|
|
log.Error("Couldn't stop DNS server: %s", err)
|
|
}
|
|
err = stopDHCPServer()
|
|
if err != nil {
|
|
log.Error("Couldn't stop DHCP server: %s", err)
|
|
}
|
|
}
|
|
|
|
// Stop HTTP server, possibly waiting for all active connections to be closed
|
|
func stopHTTPServer() {
|
|
log.Info("Stopping HTTP server...")
|
|
Context.httpsServer.shutdown = true
|
|
if Context.httpsServer.server != nil {
|
|
_ = Context.httpsServer.server.Shutdown(context.TODO())
|
|
}
|
|
if Context.httpServer != nil {
|
|
_ = Context.httpServer.Shutdown(context.TODO())
|
|
}
|
|
|
|
if Context.auth != nil {
|
|
Context.auth.Close()
|
|
Context.auth = nil
|
|
}
|
|
|
|
log.Info("Stopped HTTP server")
|
|
}
|
|
|
|
// This function is called before application exits
|
|
func cleanupAlways() {
|
|
if len(Context.pidFileName) != 0 {
|
|
_ = os.Remove(Context.pidFileName)
|
|
}
|
|
log.Info("Stopped")
|
|
}
|
|
|
|
// command-line arguments
|
|
type options struct {
|
|
verbose bool // is verbose logging enabled
|
|
configFilename string // path to the config file
|
|
workDir string // path to the working directory where we will store the filters data and the querylog
|
|
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
|
|
pidFile string // File name to save PID to
|
|
checkConfig bool // Check configuration and exit
|
|
disableUpdate bool // If set, don't check for updates
|
|
|
|
// 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 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},
|
|
{"host", "h", "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, reload (configuration)", func(value string) {
|
|
o.serviceControlAction = value
|
|
}, nil},
|
|
{"logfile", "l", "Path to log file. If empty: write to stdout; if 'syslog': write to system log", func(value string) {
|
|
o.logFile = value
|
|
}, nil},
|
|
{"pidfile", "", "Path to a file where PID is stored", func(value string) { o.pidFile = value }, nil},
|
|
{"check-config", "", "Check configuration and exit", nil, func() { o.checkConfig = true }},
|
|
{"no-check-update", "", "Don't check for updates", nil, func() { o.disableUpdate = true }},
|
|
{"verbose", "v", "Enable verbose output", nil, func() { o.verbose = true }},
|
|
{"version", "", "Show the version and exit", nil, func() {
|
|
fmt.Printf("AdGuardHome %s\n", versionString)
|
|
os.Exit(0)
|
|
}},
|
|
{"help", "", "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 {
|
|
val := ""
|
|
if opt.callbackWithValue != nil {
|
|
val = " VALUE"
|
|
}
|
|
if opt.shortName != "" {
|
|
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName+val, opt.description)
|
|
} else {
|
|
fmt.Printf(" %-34s %s\n", "--"+opt.longName+val, opt.description)
|
|
}
|
|
}
|
|
}
|
|
for i := 1; i < len(os.Args); i++ {
|
|
v := os.Args[i]
|
|
knownParam := false
|
|
for _, opt := range opts {
|
|
if v == "--"+opt.longName || (opt.shortName != "" && v == "-"+opt.shortName) {
|
|
if opt.callbackWithValue != nil {
|
|
if i+1 >= len(os.Args) {
|
|
log.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.Error("unknown option %v\n", v)
|
|
printHelp()
|
|
os.Exit(64)
|
|
}
|
|
}
|
|
|
|
return o
|
|
}
|
|
|
|
// 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
|
|
|
|
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" {
|
|
log.Println("AdGuard Home is available on the following addresses:")
|
|
ifaces, err := util.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)
|
|
}
|
|
}
|
|
|
|
// -------------------
|
|
// first run / install
|
|
// -------------------
|
|
func detectFirstRun() bool {
|
|
configfile := Context.configFilename
|
|
if !filepath.IsAbs(configfile) {
|
|
configfile = filepath.Join(Context.workDir, Context.configFilename)
|
|
}
|
|
_, err := os.Stat(configfile)
|
|
if !os.IsNotExist(err) {
|
|
// do nothing, file exists
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Connect to a remote server resolving hostname using our own DNS server
|
|
func customDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
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 {
|
|
con, err := dialer.DialContext(ctx, network, addr)
|
|
return con, err
|
|
}
|
|
|
|
addrs, e := Context.dnsServer.Resolve(host)
|
|
log.Debug("dnsServer.Resolve: %s: %v", host, addrs)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
|
|
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)
|
|
con, err := dialer.DialContext(ctx, network, addr)
|
|
if err != nil {
|
|
dialErrs = append(dialErrs, err)
|
|
continue
|
|
}
|
|
return con, err
|
|
}
|
|
return nil, errorx.DecorateMany(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
|
|
}
|
|
|
|
func getHTTPProxy(req *http.Request) (*url.URL, error) {
|
|
if len(config.ProxyURL) == 0 {
|
|
return nil, nil
|
|
}
|
|
return url.Parse(config.ProxyURL)
|
|
}
|