-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
web.go
285 lines (239 loc) · 7.96 KB
/
web.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package home
import (
"context"
"crypto/tls"
"io/fs"
"net"
"net/http"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/NYTimes/gziphandler"
)
// HTTP scheme constants.
const (
schemeHTTP = "http"
schemeHTTPS = "https"
)
const (
// readTimeout is the maximum duration for reading the entire request,
// including the body.
readTimeout = 60 * time.Second
// readHdrTimeout is the amount of time allowed to read request headers.
readHdrTimeout = 60 * time.Second
// writeTimeout is the maximum duration before timing out writes of the
// response.
writeTimeout = 60 * time.Second
)
type webConfig struct {
clientFS fs.FS
clientBetaFS fs.FS
BindHost net.IP
BindPort int
BetaBindPort int
PortHTTPS int
// ReadTimeout is an option to pass to http.Server for setting an
// appropriate field.
ReadTimeout time.Duration
// ReadHeaderTimeout is an option to pass to http.Server for setting an
// appropriate field.
ReadHeaderTimeout time.Duration
// WriteTimeout is an option to pass to http.Server for setting an
// appropriate field.
WriteTimeout time.Duration
firstRun bool
}
// 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
httpServer *http.Server // HTTP module
httpsServer HTTPSServer // HTTPS module
// handlerBeta is the handler for new client.
handlerBeta http.Handler
// installerBeta is the pre-install handler for new client.
installerBeta http.Handler
// httpServerBeta is a server for new client.
httpServerBeta *http.Server
}
// CreateWeb - create module
func CreateWeb(conf *webConfig) *Web {
log.Info("Initialize web module")
w := Web{}
w.conf = conf
clientFS := http.FileServer(http.FS(conf.clientFS))
betaClientFS := http.FileServer(http.FS(conf.clientBetaFS))
// if not configured, redirect / to /install.html, otherwise redirect /install.html to /
Context.mux.Handle("/", withMiddlewares(clientFS, gziphandler.GzipHandler, optionalAuthHandler, postInstallHandler))
w.handlerBeta = withMiddlewares(betaClientFS, gziphandler.GzipHandler, optionalAuthHandler, postInstallHandler)
// 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 ")
Context.mux.Handle("/install.html", preInstallHandler(clientFS))
w.installerBeta = preInstallHandler(betaClientFS)
w.registerInstallHandlers()
// This must be removed in API v1.
w.registerBetaInstallHandlers()
} 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 {
return Context.web.httpsServer.server != nil ||
aghnet.CheckPort("tcp", config.BindHost, port) == nil
}
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
// if necessary.
func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
log.Debug("Web: applying new TLS configuration")
web.conf.PortHTTPS = tlsConf.PortHTTPS
web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
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 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, shutdownTimeout)
shutdownSrv(ctx, web.httpsServer.server)
cancel()
}
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() {
log.Println("AdGuard Home is available at the following addresses:")
// 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(schemeHTTP)
errs := make(chan error, 2)
hostStr := web.conf.BindHost.String()
// we need to have new instance, because after Shutdown() the Server is not usable
web.httpServer = &http.Server{
ErrorLog: log.StdLog("web: plain", log.DEBUG),
Addr: netutil.JoinHostPort(hostStr, web.conf.BindPort),
Handler: withMiddlewares(Context.mux, limitRequestBody),
ReadTimeout: web.conf.ReadTimeout,
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
WriteTimeout: web.conf.WriteTimeout,
}
go func() {
defer log.OnPanic("web: plain")
errs <- web.httpServer.ListenAndServe()
}()
web.startBetaServer(hostStr)
err := <-errs
if !errors.Is(err, http.ErrServerClosed) {
cleanupAlways()
log.Fatal(err)
}
// We use ErrServerClosed as a sign that we need to rebind on a new
// address, so go back to the start of the loop.
}
}
// startBetaServer starts the beta HTTP server if necessary.
func (web *Web) startBetaServer(hostStr string) {
if web.conf.BetaBindPort == 0 {
return
}
web.httpServerBeta = &http.Server{
ErrorLog: log.StdLog("web: plain: beta", log.DEBUG),
Addr: netutil.JoinHostPort(hostStr, web.conf.BetaBindPort),
Handler: withMiddlewares(Context.mux, limitRequestBody, web.wrapIndexBeta),
ReadTimeout: web.conf.ReadTimeout,
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
WriteTimeout: web.conf.WriteTimeout,
}
go func() {
defer log.OnPanic("web: plain: beta")
betaErr := web.httpServerBeta.ListenAndServe()
if betaErr != nil && !errors.Is(betaErr, http.ErrServerClosed) {
log.Error("starting beta http server: %s", betaErr)
}
}()
}
// Close gracefully shuts down the HTTP servers.
func (web *Web) Close(ctx context.Context) {
log.Info("stopping http server...")
web.httpsServer.cond.L.Lock()
web.httpsServer.shutdown = true
web.httpsServer.cond.L.Unlock()
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, shutdownTimeout)
defer cancel()
shutdownSrv(ctx, web.httpsServer.server)
shutdownSrv(ctx, web.httpServer)
shutdownSrv(ctx, web.httpServerBeta)
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 := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
web.httpsServer.server = &http.Server{
ErrorLog: log.StdLog("web: https", log.DEBUG),
Addr: address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{web.httpsServer.cert},
RootCAs: Context.tlsRoots,
CipherSuites: aghtls.SaferCipherSuites(),
MinVersion: tls.VersionTLS12,
},
Handler: withMiddlewares(Context.mux, limitRequestBody),
ReadTimeout: web.conf.ReadTimeout,
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
WriteTimeout: web.conf.WriteTimeout,
}
printHTTPAddresses(schemeHTTPS)
err := web.httpsServer.server.ListenAndServeTLS("", "")
if err != http.ErrServerClosed {
cleanupAlways()
log.Fatal(err)
}
}
}