1
0
mirror of https://github.com/labstack/echo.git synced 2026-03-12 15:46:17 +02:00
Files
echo/server.go

194 lines
5.8 KiB
Go

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (
stdContext "context"
"crypto/tls"
"errors"
"fmt"
"io/fs"
"log/slog"
"net"
"net/http"
"os"
"sync"
"time"
)
const (
banner = "Echo (v%s). High performance, minimalist Go web framework https://echo.labstack.com"
)
// StartConfig is for creating configured http.Server instance to start serve http(s) requests with given Echo instance
type StartConfig struct {
// Address specifies the address where listener will start listening on to serve HTTP(s) requests
Address string
// HideBanner instructs Start* method not to print banner when starting the Server.
HideBanner bool
// HidePort instructs Start* method not to print port when starting the Server.
HidePort bool
// CertFilesystem is filesystem is used to read `certFile` and `keyFile` when StartTLS method is called.
CertFilesystem fs.FS
TLSConfig *tls.Config
// ListenerNetwork is used configure on which Network listener will use.
ListenerNetwork string
// ListenerAddrFunc will be called after listener is created and started to listen for connections. This is useful in
// testing situations when server is started on random port `addres = ":0"` in that case you can get actual port where
// listener is listening on.
ListenerAddrFunc func(addr net.Addr)
// GracefulTimeout is timeout value (defaults to 10sec) graceful shutdown will wait for server to handle ongoing requests
// before shutting down the server.
GracefulTimeout time.Duration
// OnShutdownError is called when graceful shutdown results an error. for example when listeners are not shut down within
// given timeout
OnShutdownError func(err error)
// BeforeServeFunc is callback that is called just before server starts to serve HTTP request.
// Use this callback when you want to configure http.Server different timeouts/limits/etc
BeforeServeFunc func(s *http.Server) error
}
// Start starts given Handler with HTTP(s) server.
func (sc StartConfig) Start(ctx stdContext.Context, h http.Handler) error {
return sc.start(ctx, h)
}
// StartTLS starts given Handler with HTTPS server.
// If `certFile` or `keyFile` is `string` the values are treated as file paths.
// If `certFile` or `keyFile` is `[]byte` the values are treated as the certificate or key as-is.
func (sc StartConfig) StartTLS(ctx stdContext.Context, h http.Handler, certFile, keyFile any) error {
certFs := sc.CertFilesystem
if certFs == nil {
certFs = os.DirFS(".")
}
cert, err := filepathOrContent(certFile, certFs)
if err != nil {
return err
}
key, err := filepathOrContent(keyFile, certFs)
if err != nil {
return err
}
cer, err := tls.X509KeyPair(cert, key)
if err != nil {
return err
}
if sc.TLSConfig == nil {
sc.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
NextProtos: []string{"h2"},
//NextProtos: []string{"http/1.1"}, // Disallow "h2", allow http
}
}
sc.TLSConfig.Certificates = []tls.Certificate{cer}
return sc.start(ctx, h)
}
// start starts handler with HTTP(s) server.
func (sc StartConfig) start(ctx stdContext.Context, h http.Handler) error {
var logger *slog.Logger
if e, ok := h.(*Echo); ok {
logger = e.Logger
} else {
logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
}
server := http.Server{
Handler: h,
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
// defaults for GoSec rule G112 // https://github.com/securego/gosec
// G112 (CWE-400): Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
listenerNetwork := sc.ListenerNetwork
if listenerNetwork == "" {
listenerNetwork = "tcp"
}
var listener net.Listener
var err error
if sc.TLSConfig != nil {
listener, err = tls.Listen(listenerNetwork, sc.Address, sc.TLSConfig)
} else {
listener, err = net.Listen(listenerNetwork, sc.Address)
}
if err != nil {
return err
}
if sc.ListenerAddrFunc != nil {
sc.ListenerAddrFunc(listener.Addr())
}
if sc.BeforeServeFunc != nil {
if err := sc.BeforeServeFunc(&server); err != nil {
_ = listener.Close()
return err
}
}
if !sc.HideBanner {
bannerText := fmt.Sprintf(banner, Version)
logger.Info(bannerText, "version", Version)
}
if !sc.HidePort {
logger.Info("http(s) server started", "address", listener.Addr().String())
}
wg := sync.WaitGroup{}
defer wg.Wait() // wait for graceful shutdown goroutine to finish
gCtx, cancel := stdContext.WithCancel(ctx) // end graceful goroutine when Serve returns early
defer cancel()
if sc.GracefulTimeout >= 0 {
wg.Add(1)
go func() {
defer wg.Done()
gracefulShutdown(gCtx, &sc, &server, logger)
}()
}
if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
func filepathOrContent(fileOrContent any, certFilesystem fs.FS) (content []byte, err error) {
switch v := fileOrContent.(type) {
case string:
return fs.ReadFile(certFilesystem, v)
case []byte:
return v, nil
default:
return nil, ErrInvalidCertOrKeyType
}
}
func gracefulShutdown(shutdownCtx stdContext.Context, sc *StartConfig, server *http.Server, logger *slog.Logger) {
<-shutdownCtx.Done() // wait until shutdown context is closed.
// note: is server if closed by other means this method is still run but is good as no-op
timeout := sc.GracefulTimeout
if timeout == 0 {
timeout = 10 * time.Second
}
waitShutdownCtx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout)
defer cancel()
if err := server.Shutdown(waitShutdownCtx); err != nil {
// we end up here when listeners are not shut down within given timeout
if sc.OnShutdownError != nil {
sc.OnShutdownError(err)
return
}
logger.Error("failed to shut down server within given timeout", "error", err)
}
}