/* * 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 }