mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-26 15:50:47 -05:00
190 lines
5.0 KiB
Go
190 lines
5.0 KiB
Go
/*
|
|
* Copyright 2017-2019 Kopano and its licensors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/longsleep/go-metrics/loggedwriter"
|
|
"github.com/longsleep/go-metrics/timing"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Server is our HTTP server implementation.
|
|
type Server struct {
|
|
Config *Config
|
|
|
|
listenAddr string
|
|
logger logrus.FieldLogger
|
|
|
|
requestLog bool
|
|
}
|
|
|
|
// NewServer constructs a server from the provided parameters.
|
|
func NewServer(c *Config) (*Server, error) {
|
|
s := &Server{
|
|
Config: c,
|
|
|
|
listenAddr: c.Config.ListenAddr,
|
|
logger: c.Config.Logger,
|
|
|
|
requestLog: os.Getenv("KOPANO_DEBUG_SERVER_REQUEST_LOG") == "1",
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// AddContext adds the associated server context with cancel to the the provided
|
|
// httprouter.Handle. When the handler is done, the per Request context is
|
|
// cancelled.
|
|
func (s *Server) AddContext(parent context.Context, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
// Create per request context.
|
|
ctx, cancel := context.WithCancel(parent)
|
|
|
|
if s.requestLog {
|
|
loggedWriter := loggedwriter.NewLoggedResponseWriter(rw)
|
|
// Create per request context.
|
|
ctx = timing.NewContext(ctx, func(duration time.Duration) {
|
|
// This is the stop callback, called when complete with duration.
|
|
durationMs := float64(duration) / float64(time.Millisecond)
|
|
// Log request.
|
|
s.logger.WithFields(logrus.Fields{
|
|
"status": loggedWriter.Status(),
|
|
"method": req.Method,
|
|
"path": req.URL.Path,
|
|
"remote": req.RemoteAddr,
|
|
"duration": durationMs,
|
|
"referer": req.Referer(),
|
|
"user-agent": req.UserAgent(),
|
|
"origin": req.Header.Get("Origin"),
|
|
}).Debug("HTTP request complete")
|
|
})
|
|
rw = loggedWriter
|
|
}
|
|
|
|
// Run the request.
|
|
next.ServeHTTP(rw, req.WithContext(ctx))
|
|
|
|
// Cancel per request context when done.
|
|
cancel()
|
|
})
|
|
}
|
|
|
|
// AddRoutes add the associated Servers URL routes to the provided router with
|
|
// the provided context.Context.
|
|
func (s *Server) AddRoutes(ctx context.Context, router *mux.Router) {
|
|
// TODO(longsleep): Add subpath support to all handlers and paths.
|
|
router.HandleFunc("/health-check", s.HealthCheckHandler)
|
|
|
|
for _, route := range s.Config.Routes {
|
|
route.AddRoutes(ctx, router)
|
|
}
|
|
if s.Config.Handler != nil {
|
|
// Delegate rest to provider which is also a handler.
|
|
router.NotFoundHandler = s.Config.Handler
|
|
}
|
|
}
|
|
|
|
// Serve starts all the accociated servers resources and listeners and blocks
|
|
// forever until signals or error occurs. Returns error and gracefully stops
|
|
// all HTTP listeners before return.
|
|
func (s *Server) Serve(ctx context.Context) error {
|
|
serveCtx, serveCtxCancel := context.WithCancel(ctx)
|
|
defer serveCtxCancel()
|
|
|
|
logger := s.logger
|
|
|
|
errCh := make(chan error, 2)
|
|
exitCh := make(chan bool, 1)
|
|
signalCh := make(chan os.Signal)
|
|
|
|
router := mux.NewRouter()
|
|
s.AddRoutes(serveCtx, router)
|
|
|
|
// HTTP listener.
|
|
srv := &http.Server{
|
|
Handler: s.AddContext(serveCtx, router),
|
|
}
|
|
|
|
logger.WithField("listenAddr", s.listenAddr).Infoln("starting http listener")
|
|
listener, err := net.Listen("tcp", s.listenAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logger.Infoln("ready to handle requests")
|
|
|
|
go func() {
|
|
serveErr := srv.Serve(listener)
|
|
if serveErr != nil {
|
|
errCh <- serveErr
|
|
}
|
|
|
|
logger.Debugln("http listener stopped")
|
|
close(exitCh)
|
|
}()
|
|
|
|
// Wait for exit or error.
|
|
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
|
|
select {
|
|
case err = <-errCh:
|
|
// breaks
|
|
case reason := <-signalCh:
|
|
logger.WithField("signal", reason).Warnln("received signal")
|
|
// breaks
|
|
}
|
|
|
|
// Shutdown, server will stop to accept new connections, requires Go 1.8+.
|
|
logger.Infoln("clean server shutdown start")
|
|
shutDownCtx, shutDownCtxCancel := context.WithTimeout(ctx, 10*time.Second)
|
|
if shutdownErr := srv.Shutdown(shutDownCtx); shutdownErr != nil {
|
|
logger.WithError(shutdownErr).Warn("clean server shutdown failed")
|
|
}
|
|
|
|
// Cancel our own context, wait on managers.
|
|
serveCtxCancel()
|
|
func() {
|
|
for {
|
|
select {
|
|
case <-exitCh:
|
|
return
|
|
default:
|
|
// HTTP listener has not quit yet.
|
|
logger.Info("waiting for http listener to exit")
|
|
}
|
|
select {
|
|
case reason := <-signalCh:
|
|
logger.WithField("signal", reason).Warn("received signal")
|
|
return
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
}()
|
|
shutDownCtxCancel() // prevent leak.
|
|
|
|
return err
|
|
|
|
}
|