package home import ( "context" "crypto/tls" golog "log" "net" "net/http" "strconv" "sync" "github.com/AdguardTeam/AdGuardHome/util" "github.com/AdguardTeam/golibs/log" "github.com/NYTimes/gziphandler" "github.com/gobuffalo/packr" ) type WebConfig struct { firstRun bool BindHost string BindPort int PortHTTPS int } // HTTPSServer - HTTPS Server type HTTPSServer struct { server *http.Server cond *sync.Cond condLock sync.Mutex shutdown bool // if TRUE, don't restart the server enabled bool cert tls.Certificate } // Web - module object type Web struct { conf *WebConfig forceHTTPS bool portHTTPS int httpServer *http.Server // HTTP module httpsServer HTTPSServer // HTTPS module errLogger *golog.Logger } // Proxy between Go's "log" and "golibs/log" type logWriter struct { } // HTTP server calls this function to log an error func (w *logWriter) Write(p []byte) (int, error) { log.Debug("Web: %s", string(p)) return 0, nil } // CreateWeb - create module func CreateWeb(conf *WebConfig) *Web { log.Info("Initialize web module") w := Web{} w.conf = conf lw := logWriter{} w.errLogger = golog.New(&lw, "", 0) // 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 conf.firstRun { log.Info("This is the first launch of AdGuard Home, redirecting everything to /install.html ") http.Handle("/install.html", preInstallHandler(http.FileServer(box))) w.registerInstallHandlers() } else { registerControlHandlers() } w.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock) return &w } // WebCheckPortAvailable - check if port is available // BUT: if we are already using this port, no need func WebCheckPortAvailable(port int) bool { alreadyRunning := false if Context.web.httpsServer.server != nil { alreadyRunning = true } if !alreadyRunning { err := util.CheckPortAvailable(config.BindHost, port) if err != nil { return false } } return true } // TLSConfigChanged - called when TLS configuration has changed func (web *Web) TLSConfigChanged(tlsConf tlsConfigSettings) { log.Debug("Web: applying new TLS configuration") web.conf.PortHTTPS = tlsConf.PortHTTPS web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0) web.portHTTPS = tlsConf.PortHTTPS enabled := tlsConf.Enabled && tlsConf.PortHTTPS != 0 && len(tlsConf.PrivateKeyData) != 0 && len(tlsConf.CertificateChainData) != 0 var cert tls.Certificate var err error if enabled { cert, err = tls.X509KeyPair(tlsConf.CertificateChainData, tlsConf.PrivateKeyData) if err != nil { log.Fatal(err) } } web.httpsServer.cond.L.Lock() if web.httpsServer.server != nil { _ = web.httpsServer.server.Shutdown(context.TODO()) } web.httpsServer.enabled = enabled web.httpsServer.cert = cert web.httpsServer.cond.Broadcast() web.httpsServer.cond.L.Unlock() } // Start - start serving HTTP requests func (web *Web) Start() { // for https, we have a separate goroutine loop go web.tlsServerLoop() // this loop is used as an ability to change listening host and/or port for !web.httpsServer.shutdown { printHTTPAddresses("http") // we need to have new instance, because after Shutdown() the Server is not usable address := net.JoinHostPort(web.conf.BindHost, strconv.Itoa(web.conf.BindPort)) web.httpServer = &http.Server{ ErrorLog: web.errLogger, Addr: address, } err := web.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 } } // Close - stop HTTP server, possibly waiting for all active connections to be closed func (web *Web) Close() { log.Info("Stopping HTTP server...") web.httpsServer.cond.L.Lock() web.httpsServer.shutdown = true web.httpsServer.cond.L.Unlock() if web.httpsServer.server != nil { _ = web.httpsServer.server.Shutdown(context.TODO()) } if web.httpServer != nil { _ = web.httpServer.Shutdown(context.TODO()) } log.Info("Stopped HTTP server") } func (web *Web) tlsServerLoop() { for { web.httpsServer.cond.L.Lock() if web.httpsServer.shutdown { web.httpsServer.cond.L.Unlock() break } // this mechanism doesn't let us through until all conditions are met for !web.httpsServer.enabled { // sleep until necessary data is supplied web.httpsServer.cond.Wait() if web.httpsServer.shutdown { web.httpsServer.cond.L.Unlock() return } } web.httpsServer.cond.L.Unlock() // prepare HTTPS server address := net.JoinHostPort(web.conf.BindHost, strconv.Itoa(web.conf.PortHTTPS)) web.httpsServer.server = &http.Server{ ErrorLog: web.errLogger, Addr: address, TLSConfig: &tls.Config{ Certificates: []tls.Certificate{web.httpsServer.cert}, MinVersion: tls.VersionTLS12, RootCAs: Context.tlsRoots, CipherSuites: Context.tlsCiphers, }, } printHTTPAddresses("https") err := web.httpsServer.server.ListenAndServeTLS("", "") if err != http.ErrServerClosed { cleanupAlways() log.Fatal(err) } } }