forked from andreaskoch/allmark
-
Notifications
You must be signed in to change notification settings - Fork 0
/
server.go
419 lines (316 loc) · 12.2 KB
/
server.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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
// Copyright 2015 Andreas Koch. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package server contains a web server that can serve an instance of
// the dataaccess.Repository interface via HTTP and HTTPs.
package server
import (
"github.com/andreaskoch/allmark/common/config"
"github.com/andreaskoch/allmark/common/logger"
"github.com/andreaskoch/allmark/dataaccess"
"github.com/andreaskoch/allmark/services/converter/markdowntohtml"
"github.com/andreaskoch/allmark/services/converter/markdowntohtml/imageprovider"
"github.com/andreaskoch/allmark/services/parser"
"github.com/andreaskoch/allmark/services/thumbnail"
"github.com/andreaskoch/allmark/web/handlers"
"github.com/andreaskoch/allmark/web/header"
"github.com/andreaskoch/allmark/web/orchestrator"
"github.com/andreaskoch/allmark/web/view/templates"
"github.com/andreaskoch/allmark/web/webpaths"
"fmt"
"github.com/gorilla/mux"
"github.com/skratchdot/open-golang/open"
"net"
"net/http"
"strings"
)
// New creates a new Server instance for the given repository.
func New(logger logger.Logger, config config.Config, repository dataaccess.Repository, parser parser.Parser, thumbnailIndex *thumbnail.Index) (*Server, error) {
patherFactory := webpaths.NewFactory(logger, repository)
webPathProvider := webpaths.NewWebPathProvider(patherFactory, handlers.BasePath, handlers.TagPathPrefix)
// image provider
imageProvider := imageprovider.NewImageProvider(webPathProvider.AbsolutePather("/"), thumbnailIndex)
// converter
converter := markdowntohtml.New(logger, imageProvider)
orchestratorFactory := orchestrator.NewFactory(logger, config, repository, parser, converter, webPathProvider)
reindexInterval := config.Indexing.IntervalInSeconds
headerWriterFactory := header.NewHeaderWriterFactory(reindexInterval)
templateProvider := templates.NewProvider(config.TemplatesFolder())
requestHandlers := handlers.GetBaseHandlers(logger, config, templateProvider, *orchestratorFactory, headerWriterFactory)
return &Server{
logger: logger,
config: config,
headerWriterFactory: headerWriterFactory,
requestHandlers: requestHandlers,
}, nil
}
// Server represents a web server instance for a given repository.
type Server struct {
logger logger.Logger
config config.Config
headerWriterFactory header.WriterFactory
requestHandlers handlers.HandlerList
}
// Start starts the current web server.
func (server *Server) Start() chan error {
result := make(chan error)
standardRequestRouter := server.getStandardRequestRouter()
// bindings
httpEndpoint, httpEnabled := server.httpEndpoint()
httpsEndpoint, httpsEnabled := server.httpsEndpoint()
// abort if no tcp bindings are configured
if len(httpEndpoint.Bindings()) == 0 && len(httpsEndpoint.Bindings()) == 0 {
result <- fmt.Errorf("No TCP bindings configured")
return result
}
uniqueURLs := make(map[string]string)
// http
if httpEnabled {
for _, tcpBinding := range httpEndpoint.Bindings() {
tcpBinding.AssignFreePort()
tcpAddr := tcpBinding.GetTCPAddress()
address := tcpAddr.String()
// start listening
go func() {
server.logger.Info("HTTP Endpoint: %s", address)
if httpEndpoint.ForceHTTPS() {
// Redirect HTTP → HTTPS
redirectTarget := httpsEndpoint.DefaultURL()
httpsRedirectRouter := server.getRedirectRouter(redirectTarget, standardRequestRouter)
if err := http.ListenAndServe(address, httpsRedirectRouter); err != nil {
result <- fmt.Errorf("Server failed with error: %v", err)
} else {
result <- nil
}
} else {
// Standard HTTP Request Router
if err := http.ListenAndServe(address, standardRequestRouter); err != nil {
result <- fmt.Errorf("Server failed with error: %v", err)
} else {
result <- nil
}
}
}()
// store the URL for later opening
if httpsEnabled == false {
endpointURL := httpEndpoint.DefaultURL()
uniqueURLs[endpointURL] = endpointURL
}
}
}
// https
if httpsEnabled {
for _, tcpBinding := range httpsEndpoint.Bindings() {
tcpBinding.AssignFreePort()
tcpAddr := tcpBinding.GetTCPAddress()
address := tcpAddr.String()
// start listening
go func() {
server.logger.Info("HTTPS Endpoint: %s", address)
// Standard HTTPS Request Router
if err := http.ListenAndServeTLS(address, httpsEndpoint.CertFilePath(), httpsEndpoint.KeyFilePath(), standardRequestRouter); err != nil {
result <- fmt.Errorf("Server failed with error: %v", err)
} else {
result <- nil
}
}()
// store the URL for later opening
endpointURL := httpsEndpoint.DefaultURL()
uniqueURLs[endpointURL] = endpointURL
}
}
// docx conversion endpoint (unencrypted, no authentication)
if server.config.Conversion.DOCX.IsEnabled() {
// start listening
go func() {
conversionEndpointTCPAddress := server.config.Conversion.EndpointBinding().GetTCPAddress()
conversionEndpointAddress := conversionEndpointTCPAddress.String()
server.logger.Info("Docx Conversion Endpoint: %s", conversionEndpointAddress)
// Standard HTTPS Request Router
if err := http.ListenAndServe(conversionEndpointAddress, server.getLocalRequestRouter()); err != nil {
result <- fmt.Errorf("Docx Conversion endpoint failed with error: %v", err)
} else {
result <- nil
}
}()
}
// open HTTP URL(s) in a browser
for _, url := range uniqueURLs {
server.logger.Info("Open URL: %s", url)
go open.Run(url)
}
return result
}
// getRedirectRouter returns a router which redirects all requests to the url with the given base.
func (server *Server) getRedirectRouter(baseURITarget string, baseHandler http.Handler) *mux.Router {
redirectRouter := mux.NewRouter()
for _, requestHandler := range handlers.GetRedirectHandlers(server.logger, baseURITarget, baseHandler) {
requestRoute := requestHandler.Route
requestHandler := requestHandler.Handler
redirectRouter.Handle(requestRoute, requestHandler)
}
return redirectRouter
}
// Get an instance of the standard request router for all repository related routes.
func (server *Server) getStandardRequestRouter() *mux.Router {
// register requst routers
requestRouter := mux.NewRouter()
for _, requestHandler := range server.requestHandlers {
requestRoute := requestHandler.Route
requestHandler := requestHandler.Handler
// add logging
requestHandler = handlers.LogRequests(requestHandler)
// add compression
requestHandler = handlers.CompressResponses(requestHandler)
// add authentication
if _, httpsEnabled := server.httpsEndpoint(); httpsEnabled && server.config.AuthenticationIsEnabled() {
secretProvider := server.config.GetAuthenticationUserStore()
if secretProvider == nil {
panic("Authentication is enabled but the supplied secret provider is nil.")
}
requestHandler = handlers.RequireDigestAuthentication(server.logger, requestHandler, secretProvider)
}
requestRouter.Handle(requestRoute, requestHandler)
}
return requestRouter
}
// getLocalRequestRouter returns a local request router without compression and without authentication.
func (server *Server) getLocalRequestRouter() *mux.Router {
// register requst routers
requestRouter := mux.NewRouter()
for _, requestHandler := range server.requestHandlers {
requestRoute := requestHandler.Route
requestHandler := requestHandler.Handler
// add logging
requestHandler = handlers.LogRequests(requestHandler)
requestRouter.Handle(requestRoute, requestHandler)
}
return requestRouter
}
// Get the http binding if it is enabled.
func (server *Server) httpEndpoint() (httpEndpoint HTTPEndpoint, enabled bool) {
if !server.config.Server.HTTP.Enabled {
return HTTPEndpoint{}, false
}
return HTTPEndpoint{
isSecure: false,
forceHTTPS: server.config.Server.HTTPS.HTTPSIsForced(),
tcpBindings: server.config.Server.HTTP.Bindings,
}, true
}
// Get the https binding if it is enabled.tcpBinding
func (server *Server) httpsEndpoint() (httpsEndpoint HTTPSEndpoint, enabled bool) {
if !server.config.Server.HTTPS.Enabled {
return HTTPSEndpoint{}, false
}
httpEndpoint := HTTPEndpoint{
domain: server.config.Server.DomainName,
isSecure: true,
tcpBindings: server.config.Server.HTTPS.Bindings,
}
certFilePath, keyFilePath, _ := server.config.CertificateFilePaths()
httpsEndpoint = HTTPSEndpoint{
HTTPEndpoint: httpEndpoint,
certFilePath: certFilePath,
keyFilePath: keyFilePath,
}
return httpsEndpoint, true
}
// HTTPEndpoint contains HTTP server endpoint parameters such as a domain name and TCP bindings.
type HTTPEndpoint struct {
domain string
isSecure bool
forceHTTPS bool
tcpBindings []*config.TCPBinding
}
func (endpoint *HTTPEndpoint) String() string {
return fmt.Sprintf("Domain: %q, IsSecure: %v, ForeceHTTPs: %v", endpoint.domain, endpoint.isSecure, endpoint.forceHTTPS)
}
// IsSecure returns a flag indicating whether the current HTTPEndpoint is secure (HTTPS) or not.
func (endpoint *HTTPEndpoint) IsSecure() bool {
return endpoint.isSecure
}
// Protocol returns the protocol of the current HTTPEndpoint. "https" if this endpoint is secure; otherwise "http".
func (endpoint *HTTPEndpoint) Protocol() string {
if endpoint.isSecure {
return "https"
}
return "http"
}
// ForceHTTPS returns a flag indicating whether a secure connection shall be preferred over insecure connections.
func (endpoint *HTTPEndpoint) ForceHTTPS() bool {
return endpoint.forceHTTPS
}
// Bindings returns all TCP bindings of the current HTTP endpoint.
func (endpoint *HTTPEndpoint) Bindings() []*config.TCPBinding {
return endpoint.tcpBindings
}
// DefaultURL return the default url for the current HTTP endpoint. It will include the domain name if one is configured.
// If none is configured it will use the IP address as the host name.
func (endpoint *HTTPEndpoint) DefaultURL() string {
// no point in returning a url if there are no tcp bindings
if len(endpoint.tcpBindings) == 0 {
return ""
}
// use the first tcp binding as the default
defaultBinding := *endpoint.tcpBindings[0]
// create an URL from the tcp binding if no domain is configured
if endpoint.domain == "" {
return getURL(*endpoint, defaultBinding)
}
// determine the port suffix (e.g. ":8080")
portSuffix := ""
portNumber := defaultBinding.Port
isDefaultPort := portNumber == 80 || portNumber == 443
if !isDefaultPort {
portSuffix = fmt.Sprintf(":%v", portNumber)
}
return fmt.Sprintf("%s://%s%s", endpoint.Protocol(), endpoint.domain, portSuffix)
}
// HTTPSEndpoint contains a secure version of a HTTPEndpoint with parameters for secure TLS connections such as the certificate paths.
type HTTPSEndpoint struct {
HTTPEndpoint
certFilePath string
keyFilePath string
}
// CertFilePath returns the SSL certificate file (e.g. "cert.pem") name of this HTTPSEndpoint.
func (endpoint *HTTPSEndpoint) CertFilePath() string {
return endpoint.certFilePath
}
// KeyFilePath returns the SSL certificate key file name (e.g. "cert.key") of this HTTPSEndpoint.
func (endpoint *HTTPSEndpoint) KeyFilePath() string {
return endpoint.keyFilePath
}
// getURL returns the formatted URL (e.g. "https://localhost:8080") for the given TCP binding,
// using the IP address as the hostname.
func getURL(endpoint HTTPEndpoint, tcpBinding config.TCPBinding) string {
tcpAddress := tcpBinding.GetTCPAddress()
hostname := tcpAddress.String()
// don't use wildcard addresses for the URL, use localhost instead
if isWildcardAddress(tcpAddress.IP) {
if isIPv6Address(tcpAddress.IP) {
// IPv6 addresses are wrapped in brackets
hostname = strings.Replace(hostname, fmt.Sprintf("[%s]", tcpAddress.IP.String()), "localhost", 1)
} else {
hostname = strings.Replace(hostname, tcpAddress.IP.String(), "localhost", 1)
}
}
return fmt.Sprintf("%s://%s", endpoint.Protocol(), hostname)
}
// isIPv6Address checks if the given IP address is a IPv6 address or not.
func isIPv6Address(ip net.IP) bool {
// if the ip cannot be converted to IPv4 it must be an IPv6 address
return ip.To4() == nil
}
// isWildcardAddress checks if the supplied ip is a "source address for this host on this network"
// (see: RFC 5735, Section 3, example: 0.0.0.0)
func isWildcardAddress(ip net.IP) bool {
switch ip.String() {
case "0.0.0.0":
return true
case "::":
return true
default:
return false
}
}