88853b76d9
Close #458 * commit '2e054b673264bdb563abb6478979d00e76e52c8d': + config: new setting 'http_proxy'
673 lines
19 KiB
Go
673 lines
19 KiB
Go
package home
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"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
|
|
|
|
// 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
|
|
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() {
|
|
<-Context.appSignalChannel
|
|
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"
|
|
}
|
|
|
|
// Init some of the Context fields right away
|
|
Context.transport = &http.Transport{
|
|
DialContext: customDialContext,
|
|
Proxy: getHTTPProxy,
|
|
}
|
|
Context.client = &http.Client{
|
|
Timeout: time.Minute * 5,
|
|
Transport: Context.transport,
|
|
}
|
|
|
|
// 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()
|
|
|
|
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 !Context.firstRun {
|
|
// Save the updated config
|
|
err := config.write()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
|
|
Context.pidFileName = args.pidFile
|
|
}
|
|
|
|
// 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)))))
|
|
registerControlHandlers()
|
|
|
|
// 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()
|
|
}
|
|
|
|
Context.httpsServer.cond = sync.NewCond(&Context.httpsServer.Mutex)
|
|
|
|
// 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
|
|
}
|
|
|
|
// wait indefinitely for other go-routines to complete their job
|
|
select {}
|
|
}
|
|
|
|
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,
|
|
},
|
|
}
|
|
|
|
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")
|
|
|
|
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())
|
|
}
|
|
_ = Context.httpServer.Shutdown(context.TODO())
|
|
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", 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)
|
|
}
|