-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhttp_server.go
More file actions
517 lines (464 loc) · 16.7 KB
/
Copy pathhttp_server.go
File metadata and controls
517 lines (464 loc) · 16.7 KB
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
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
// Package restapi provides a lightweight HTTP/HTTPS server built on Gin with optional multi-address listening and helper utilities.
package restapi
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log/slog"
"math/big"
"net"
"net/http"
"os"
"sync"
"time"
"github.com/dan-sherwin/go-rest-api-server/middlewares"
"github.com/gin-gonic/gin"
)
const (
// LevelDebug corresponds to slog.LevelDebug.
LevelDebug = middlewares.LevelDebug
// LevelInfo corresponds to slog.LevelInfo.
LevelInfo = middlewares.LevelInfo
// LevelWarn corresponds to slog.LevelWarn.
LevelWarn = middlewares.LevelWarn
// LevelError corresponds to slog.LevelError.
LevelError = middlewares.LevelError
// LevelOff is a custom level used to disable request logging.
LevelOff = middlewares.LevelOff
)
// httpApp is the instance of the Gin Engine used for HTTP routing and handling.
//
// httpServer represents the HTTP server that will serve requests over the network.
//
// ListeningAddress is the address and port the server listens on for incoming HTTP connections.
var (
httpApp *gin.Engine
// Legacy single-server handles retained for backward compatibility
httpServer *http.Server
httpsServer *http.Server
// New: support multiple interfaces/ports
httpServers []*http.Server
httpsServers []*http.Server
// Legacy single-address configuration (still supported)
ListeningAddress = "0.0.0.0:5555"
HTTPSListeningAddress = "0.0.0.0:5556"
// New: optional plural address config; if non-empty, takes precedence over the single address
ListeningAddresses []string
HTTPSListeningAddresses []string
disablePing = false
// LogGetRequests determines whether or not to log GET requests.
LogGetRequests = false
// LogLevel specifies the level at which requests should be logged.
LogLevel = LevelInfo
// track self-signed TLS files we generated so we can clean them up on shutdown
autoGeneratedTLSFiles bool
generatedCertPath string
generatedKeyPath string
once sync.Once
)
// init configures the HTTP server with necessary middlewares including CORS, request ID generation, logging, and no-cache headers, and initializes the Gin engine with recovery and trusted proxy settings.
func init() {
slog.Debug("initializing http server")
gin.SetMode(gin.ReleaseMode)
httpApp = gin.New()
httpApp.Use(gin.Recovery())
_ = httpApp.SetTrustedProxies(nil)
httpApp.Use(gin.Recovery(), middlewares.CORSMiddleware(), middlewares.RequestLoggerWithDynamicConfig(&LogGetRequests, &LogLevel), middlewares.NoCache())
}
// StartHttpServer initializes and starts one or more HTTP servers at the configured listening addresses.
// If `ListeningAddresses` is non-empty, a server is started for each address; otherwise, the legacy
// single `ListeningAddress` is used. Servers run in separate goroutines for non-blocking operation.
//
//revive:disable-next-line var-naming
func StartHttpServer() {
// Resolve addresses
addrs := ListeningAddresses
if len(addrs) == 0 {
addrs = []string{ListeningAddress}
}
slog.Info(fmt.Sprintf("starting HTTP service on %v", addrs))
once.Do(setupRoutes)
// Clear previous slice in case of reuse
httpServers = nil
for _, addr := range addrs {
addr := addr // capture
// Maintain legacy single httpServer for the first address for backward compat
server := &http.Server{Addr: addr, Handler: httpApp.Handler()}
if httpServer == nil {
httpServer = server
}
httpServers = append(httpServers, server)
go func(s *http.Server) {
slog.Info("listening and serving HTTP on " + addr)
if err := s.ListenAndServe(); err != nil {
if err.Error() != "http: Server closed" {
slog.Error("error occurred while setting up the HTTP server. " + err.Error())
os.Exit(1)
}
}
}(server)
}
}
// StartHttpsServer initializes and starts one or more HTTPS servers with TLS using the provided certificate and key files.
// If `HTTPSListeningAddresses` is non-empty, a server is started for each address; otherwise, the legacy
// single `HTTPSListeningAddress` is used. Servers run asynchronously and log startup/errors.
//
//revive:disable-next-line var-naming
func StartHttpsServer(certFile, keyFile string, autoSelfSigned bool) {
// Resolve addresses
addrs := HTTPSListeningAddresses
if len(addrs) == 0 {
addrs = []string{HTTPSListeningAddress}
}
slog.Info(fmt.Sprintf("starting TLS HTTP service on %v", addrs))
once.Do(setupRoutes)
var err error
certPath := certFile
keyPath := keyFile
// reset tracking by default
autoGeneratedTLSFiles = false
generatedCertPath = ""
generatedKeyPath = ""
if certPath == "" || keyPath == "" || !fileExists(certPath) || !fileExists(keyPath) {
if autoSelfSigned {
certPath, keyPath, err = generateSelfSignedCertFiles(certPath, keyPath)
if err != nil {
slog.Error("failed to generate self-signed certificate: " + err.Error())
os.Exit(1)
}
// remember these so we can clean them up later
autoGeneratedTLSFiles = true
generatedCertPath = certPath
generatedKeyPath = keyPath
} else {
slog.Error("TLS cert or key path not provided or not found, and autoSelfSigned is false")
os.Exit(1)
}
}
// Clear previous slice in case of reuse
httpsServers = nil
for _, addr := range addrs {
addr := addr // capture
server := &http.Server{Addr: addr, Handler: httpApp.Handler()}
if httpsServer == nil {
httpsServer = server
}
httpsServers = append(httpsServers, server)
go func(s *http.Server) {
slog.Info("listening and serving HTTPS on " + addr)
if err := s.ListenAndServeTLS(certPath, keyPath); err != nil {
if err.Error() != "http: Server closed" {
slog.Error("error occurred while setting up the HTTPS server. " + err.Error())
os.Exit(1)
}
}
}(server)
}
}
// StartHttpsServerFromStrings creates temporary certificate and key files from the provided
// PEM strings and starts the HTTPS server using those files. If both strings are empty,
// it delegates to StartHttpsServer with the provided autoSelfSigned flag. If only one of
// the strings is provided, the function logs an error and exits.
//
// The temporary files created by this function are tracked and will be removed by
// ShutdownHttpsServer or CleanupGeneratedTLSFiles.
//
//revive:disable-next-line var-naming
func StartHttpsServerFromStrings(certFileStr, keyFileStr string, autoSelfSigned bool) {
// Case: no strings provided — fall back to regular StartHttpsServer behavior
if certFileStr == "" && keyFileStr == "" {
StartHttpsServer("", "", autoSelfSigned)
return
}
// If only one of the strings is provided, it's an error
if (certFileStr == "" && keyFileStr != "") || (certFileStr != "" && keyFileStr == "") {
slog.Error("both certFileStr and keyFileStr must be provided together or both empty")
os.Exit(1)
}
// Create temp files to hold provided PEM contents
certTemp, err := os.CreateTemp("", "rest_api_server_cert_*.pem")
if err != nil {
slog.Error("failed to create temp cert file: " + err.Error())
os.Exit(1)
}
certPath := certTemp.Name()
_ = certTemp.Close()
if err := os.WriteFile(certPath, []byte(certFileStr), 0644); err != nil {
slog.Error("failed to write temp cert file: " + err.Error())
os.Exit(1)
}
keyTemp, err := os.CreateTemp("", "rest_api_server_key_*.pem")
if err != nil {
slog.Error("failed to create temp key file: " + err.Error())
os.Exit(1)
}
keyPath := keyTemp.Name()
_ = keyTemp.Close()
if err := os.WriteFile(keyPath, []byte(keyFileStr), 0600); err != nil {
slog.Error("failed to write temp key file: " + err.Error())
os.Exit(1)
}
// Start HTTPS server using the created files. We pass autoSelfSigned=false to ensure
// these exact files are used. StartHttpsServer will clear any previous tracking state,
// so we set tracking after the call so cleanup works for these temp files.
StartHttpsServer(certPath, keyPath, false)
// Track for cleanup on shutdown
autoGeneratedTLSFiles = true
generatedCertPath = certPath
generatedKeyPath = keyPath
}
// DisablePing sets the internal `disablePing` variable to true, effectively disabling the ping functionality.
func DisablePing() {
disablePing = true
}
// RegisterRouters registers multiple router configurations to the HTTP application engine. It accepts a variadic number of functions, each receiving the *gin.Engine instance for adding routes and middleware.
func RegisterRouters(registrars ...func(r *gin.Engine)) {
for _, r := range registrars {
r(httpApp)
}
}
// shutdownServersWithPolicy shuts down the provided servers using the given policy.
// If timeout <= 0, it performs a graceful shutdown with no deadline (may wait indefinitely).
// If timeout > 0, it waits up to the timeout for graceful shutdown; if that times out and force is true,
// it calls Close() to forcefully terminate remaining connections.
// Returns the first error encountered (if any).
func shutdownServersWithPolicy(servers []*http.Server, timeout time.Duration, force bool) error {
if len(servers) == 0 {
return nil
}
var wg sync.WaitGroup
var firstErr error
var mu sync.Mutex
for _, s := range servers {
if s == nil {
continue
}
wg.Add(1)
go func(srv *http.Server) {
defer wg.Done()
var err error
if timeout > 0 {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err = srv.Shutdown(ctx)
// If the shutdown exceeded the timeout and we want to force, close the server.
if err == context.DeadlineExceeded && force {
if cerr := srv.Close(); cerr != nil {
mu.Lock()
if firstErr == nil {
firstErr = cerr
}
mu.Unlock()
}
}
} else {
// indefinite graceful shutdown
err = srv.Shutdown(context.Background())
}
if err != nil && err != context.Canceled {
mu.Lock()
if firstErr == nil {
firstErr = err
}
mu.Unlock()
}
}(s)
}
wg.Wait()
return firstErr
}
// ShutdownHttpServer gracefully shuts down all running HTTP servers.
// Returns the first error encountered (if any).
//
//revive:disable-next-line var-naming
func ShutdownHttpServer() error {
servers := httpServers
// Backward compatibility: if slice is empty but legacy handle exists, use it
if len(servers) == 0 && httpServer != nil {
servers = []*http.Server{httpServer}
}
// Indefinite graceful shutdown for backward compatibility
return shutdownServersWithPolicy(servers, 0, false)
}
// setupRoutes defines HTTP routes and their corresponding handlers for the application.
func setupRoutes() {
httpApp.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"message": "Not Found",
})
})
if !disablePing {
httpApp.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
}
}
// ClientIP retrieves the client IP address from the request context. It checks the "X-Forwarded-For" header first and returns its value if present; otherwise, it falls back to using c.ClientIP().
func ClientIP(c *gin.Context) string {
buf := c.GetHeader("X-Forwarded-For")
if buf != "" {
return buf
}
return c.ClientIP()
}
// ShutdownHttpsServer gracefully shuts down all running HTTPS servers and cleans up any generated cert/key files.
// Returns the first error encountered (if any).
//
//revive:disable-next-line var-naming
func ShutdownHttpsServer() error {
servers := httpsServers
// Backward compatibility: if slice is empty but legacy handle exists, use it
if len(servers) == 0 && httpsServer != nil {
servers = []*http.Server{httpsServer}
}
// Indefinite graceful shutdown for backward compatibility
firstErr := shutdownServersWithPolicy(servers, 0, false)
// attempt cleanup regardless of shutdown success
cleanupGeneratedTLSFiles()
return firstErr
}
// ShutdownHttpServerWithTimeout attempts to gracefully shut down all running HTTP servers.
// If timeout <= 0, it waits indefinitely for a graceful shutdown (same as ShutdownHttpServer).
// If timeout > 0 and the graceful shutdown does not complete within the timeout and force is true,
// it will forcefully close remaining connections.
// Returns the first error encountered (if any).
//
//revive:disable-next-line var-naming
func ShutdownHttpServerWithTimeout(timeout time.Duration, force bool) error {
servers := httpServers
if len(servers) == 0 && httpServer != nil {
servers = []*http.Server{httpServer}
}
return shutdownServersWithPolicy(servers, timeout, force)
}
// ShutdownHttpsServerWithTimeout attempts to gracefully shut down all running HTTPS servers.
// If timeout <= 0, it waits indefinitely for a graceful shutdown (same as ShutdownHttpsServer).
// If timeout > 0 and the graceful shutdown does not complete within the timeout and force is true,
// it will forcefully close remaining connections. Any generated TLS files will be cleaned up
// regardless of shutdown success.
// Returns the first error encountered (if any).
//
//revive:disable-next-line var-naming
func ShutdownHttpsServerWithTimeout(timeout time.Duration, force bool) error {
servers := httpsServers
if len(servers) == 0 && httpsServer != nil {
servers = []*http.Server{httpsServer}
}
firstErr := shutdownServersWithPolicy(servers, timeout, force)
cleanupGeneratedTLSFiles()
return firstErr
}
// fileExists reports whether the named file exists.
func fileExists(path string) bool {
if path == "" {
return false
}
if _, err := os.Stat(path); err == nil {
return true
}
return false
}
// cleanupGeneratedTLSFiles removes any self-signed cert/key files generated by this package.
// It is safe to call multiple times. Errors are logged at debug level and otherwise ignored.
func cleanupGeneratedTLSFiles() {
if !autoGeneratedTLSFiles {
return
}
if generatedCertPath != "" {
if err := os.Remove(generatedCertPath); err != nil {
slog.Debug("failed to remove generated TLS cert file", slog.String("path", generatedCertPath), slog.String("error", err.Error()))
} else {
slog.Debug("removed generated TLS cert file", slog.String("path", generatedCertPath))
}
}
if generatedKeyPath != "" {
if err := os.Remove(generatedKeyPath); err != nil {
slog.Debug("failed to remove generated TLS key file", slog.String("path", generatedKeyPath), slog.String("error", err.Error()))
} else {
slog.Debug("removed generated TLS key file", slog.String("path", generatedKeyPath))
}
}
// clear state
autoGeneratedTLSFiles = false
generatedCertPath = ""
generatedKeyPath = ""
}
// CleanupGeneratedTLSFiles removes any self-signed cert/key files generated by StartHttpsServer.
// Intended for applications to call (e.g., via defer) if they want to ensure cleanup even without calling ShutdownHttpsServer.
func CleanupGeneratedTLSFiles() {
cleanupGeneratedTLSFiles()
}
// generateSelfSignedCertFiles generates a self-signed certificate and key, writes them to files, and returns their paths.
// If certOut or keyOut are empty, temporary files will be created in the OS temp directory.
func generateSelfSignedCertFiles(certOut, keyOut string) (string, string, error) {
// Ensure output paths
if certOut == "" {
f, err := os.CreateTemp("", "rest_api_server_cert_*.pem")
if err != nil {
return "", "", err
}
_ = f.Close()
certOut = f.Name()
}
if keyOut == "" {
f, err := os.CreateTemp("", "rest_api_server_key_*.pem")
if err != nil {
return "", "", err
}
_ = f.Close()
keyOut = f.Name()
}
// Generate a private key
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", "", err
}
// Create certificate template
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return "", "", err
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "rest-api-server",
Organization: []string{"Spacelink"},
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
// Add common hostnames and IPs
hosts := []string{"localhost"}
template.DNSNames = append(template.DNSNames, hosts...)
for _, ipStr := range []string{"127.0.0.1", "::1"} {
if ip := net.ParseIP(ipStr); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
}
}
// Create the certificate
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return "", "", err
}
// Write cert file
if err := os.WriteFile(certOut, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), 0644); err != nil {
return "", "", err
}
// Write key file
keyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
if err := os.WriteFile(keyOut, keyPem, 0600); err != nil {
return "", "", err
}
return certOut, keyOut, nil
}