github.com/crowdsecurity/crowdsec@v1.6.1/pkg/apiserver/apiserver.go (about)

     1  package apiserver
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/gin-gonic/gin"
    16  	"github.com/go-co-op/gocron"
    17  	log "github.com/sirupsen/logrus"
    18  	"gopkg.in/natefinch/lumberjack.v2"
    19  	"gopkg.in/tomb.v2"
    20  
    21  	"github.com/crowdsecurity/go-cs-lib/trace"
    22  
    23  	"github.com/crowdsecurity/crowdsec/pkg/apiserver/controllers"
    24  	v1 "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
    25  	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
    26  	"github.com/crowdsecurity/crowdsec/pkg/csplugin"
    27  	"github.com/crowdsecurity/crowdsec/pkg/database"
    28  	"github.com/crowdsecurity/crowdsec/pkg/types"
    29  )
    30  
    31  const keyLength = 32
    32  
    33  type APIServer struct {
    34  	URL            string
    35  	UnixSocket     string
    36  	TLS            *csconfig.TLSCfg
    37  	dbClient       *database.Client
    38  	logFile        string
    39  	controller     *controllers.Controller
    40  	flushScheduler *gocron.Scheduler
    41  	router         *gin.Engine
    42  	httpServer     *http.Server
    43  	apic           *apic
    44  	papi           *Papi
    45  	httpServerTomb tomb.Tomb
    46  	consoleConfig  *csconfig.ConsoleConfig
    47  }
    48  
    49  func recoverFromPanic(c *gin.Context) {
    50  	err := recover()
    51  	if err == nil {
    52  		return
    53  	}
    54  
    55  	// Check for a broken connection, as it is not really a
    56  	// condition that warrants a panic stack trace.
    57  	brokenPipe := false
    58  
    59  	if ne, ok := err.(*net.OpError); ok {
    60  		if se, ok := ne.Err.(*os.SyscallError); ok {
    61  			if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
    62  				brokenPipe = true
    63  			}
    64  		}
    65  	}
    66  
    67  	// because of https://github.com/golang/net/blob/39120d07d75e76f0079fe5d27480bcb965a21e4c/http2/server.go
    68  	// and because it seems gin doesn't handle those neither, we need to "hand define" some errors to properly catch them
    69  	if strErr, ok := err.(error); ok {
    70  		// stolen from http2/server.go in x/net
    71  		var (
    72  			errClientDisconnected = errors.New("client disconnected")
    73  			errClosedBody         = errors.New("body closed by handler")
    74  			errHandlerComplete    = errors.New("http2: request body closed due to handler exiting")
    75  			errStreamClosed       = errors.New("http2: stream closed")
    76  		)
    77  
    78  		if errors.Is(strErr, errClientDisconnected) ||
    79  			errors.Is(strErr, errClosedBody) ||
    80  			errors.Is(strErr, errHandlerComplete) ||
    81  			errors.Is(strErr, errStreamClosed) {
    82  			brokenPipe = true
    83  		}
    84  	}
    85  
    86  	if brokenPipe {
    87  		log.Warningf("client %s disconnected : %s", c.ClientIP(), err)
    88  		c.Abort()
    89  	} else {
    90  		filename := trace.WriteStackTrace(err)
    91  		log.Warningf("client %s error : %s", c.ClientIP(), err)
    92  		log.Warningf("stacktrace written to %s, please join to your issue", filename)
    93  		c.AbortWithStatus(http.StatusInternalServerError)
    94  	}
    95  }
    96  
    97  // CustomRecoveryWithWriter returns a middleware for a writer that recovers from any panics and writes a 500 if there was one.
    98  func CustomRecoveryWithWriter() gin.HandlerFunc {
    99  	return func(c *gin.Context) {
   100  		defer recoverFromPanic(c)
   101  		c.Next()
   102  	}
   103  }
   104  
   105  // XXX: could be a method of LocalApiServerCfg
   106  func newGinLogger(config *csconfig.LocalApiServerCfg) (*log.Logger, string, error) {
   107  	clog := log.New()
   108  
   109  	if err := types.ConfigureLogger(clog); err != nil {
   110  		return nil, "", fmt.Errorf("while configuring gin logger: %w", err)
   111  	}
   112  
   113  	if config.LogLevel != nil {
   114  		clog.SetLevel(*config.LogLevel)
   115  	}
   116  
   117  	if config.LogMedia != "file" {
   118  		return clog, "", nil
   119  	}
   120  
   121  	// Log rotation
   122  
   123  	logFile := filepath.Join(config.LogDir, "crowdsec_api.log")
   124  	log.Debugf("starting router, logging to %s", logFile)
   125  
   126  	logger := &lumberjack.Logger{
   127  		Filename:   logFile,
   128  		MaxSize:    500, // megabytes
   129  		MaxBackups: 3,
   130  		MaxAge:     28,   // days
   131  		Compress:   true, // disabled by default
   132  	}
   133  
   134  	if config.LogMaxSize != 0 {
   135  		logger.MaxSize = config.LogMaxSize
   136  	}
   137  
   138  	if config.LogMaxFiles != 0 {
   139  		logger.MaxBackups = config.LogMaxFiles
   140  	}
   141  
   142  	if config.LogMaxAge != 0 {
   143  		logger.MaxAge = config.LogMaxAge
   144  	}
   145  
   146  	if config.CompressLogs != nil {
   147  		logger.Compress = *config.CompressLogs
   148  	}
   149  
   150  	clog.SetOutput(logger)
   151  
   152  	return clog, logFile, nil
   153  }
   154  
   155  // NewServer creates a LAPI server.
   156  // It sets up a gin router, a database client, and a controller.
   157  func NewServer(config *csconfig.LocalApiServerCfg) (*APIServer, error) {
   158  	var flushScheduler *gocron.Scheduler
   159  
   160  	dbClient, err := database.NewClient(config.DbConfig)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("unable to init database client: %w", err)
   163  	}
   164  
   165  	if config.DbConfig.Flush != nil {
   166  		flushScheduler, err = dbClient.StartFlushScheduler(config.DbConfig.Flush)
   167  		if err != nil {
   168  			return nil, err
   169  		}
   170  	}
   171  
   172  	if log.GetLevel() < log.DebugLevel {
   173  		gin.SetMode(gin.ReleaseMode)
   174  	}
   175  
   176  	router := gin.New()
   177  
   178  	router.ForwardedByClientIP = false
   179  
   180  	// set the remore address of the request to 127.0.0.1 if it comes from a unix socket
   181  	router.Use(func(c *gin.Context) {
   182  		if c.Request.RemoteAddr == "@" {
   183  			c.Request.RemoteAddr = "127.0.0.1:65535"
   184  		}
   185  	})
   186  
   187  	if config.TrustedProxies != nil && config.UseForwardedForHeaders {
   188  		if err = router.SetTrustedProxies(*config.TrustedProxies); err != nil {
   189  			return nil, fmt.Errorf("while setting trusted_proxies: %w", err)
   190  		}
   191  
   192  		router.ForwardedByClientIP = true
   193  	}
   194  
   195  	// The logger that will be used by handlers
   196  	clog, logFile, err := newGinLogger(config)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	gin.DefaultErrorWriter = clog.WriterLevel(log.ErrorLevel)
   202  	gin.DefaultWriter = clog.Writer()
   203  
   204  	router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
   205  		return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
   206  			param.ClientIP,
   207  			param.TimeStamp.Format(time.RFC1123),
   208  			param.Method,
   209  			param.Path,
   210  			param.Request.Proto,
   211  			param.StatusCode,
   212  			param.Latency,
   213  			param.Request.UserAgent(),
   214  			param.ErrorMessage,
   215  		)
   216  	}))
   217  
   218  	router.NoRoute(func(c *gin.Context) {
   219  		c.JSON(http.StatusNotFound, gin.H{"message": "Page or Method not found"})
   220  	})
   221  	router.Use(CustomRecoveryWithWriter())
   222  
   223  	controller := &controllers.Controller{
   224  		DBClient:                      dbClient,
   225  		Ectx:                          context.Background(),
   226  		Router:                        router,
   227  		Profiles:                      config.Profiles,
   228  		Log:                           clog,
   229  		ConsoleConfig:                 config.ConsoleConfig,
   230  		DisableRemoteLapiRegistration: config.DisableRemoteLapiRegistration,
   231  	}
   232  
   233  	var (
   234  		apiClient  *apic
   235  		papiClient *Papi
   236  	)
   237  
   238  	controller.AlertsAddChan = nil
   239  	controller.DecisionDeleteChan = nil
   240  
   241  	if config.OnlineClient != nil && config.OnlineClient.Credentials != nil {
   242  		log.Printf("Loading CAPI manager")
   243  
   244  		apiClient, err = NewAPIC(config.OnlineClient, dbClient, config.ConsoleConfig, config.CapiWhitelists)
   245  		if err != nil {
   246  			return nil, err
   247  		}
   248  
   249  		log.Infof("CAPI manager configured successfully")
   250  
   251  		controller.AlertsAddChan = apiClient.AlertsAddChan
   252  
   253  		if config.ConsoleConfig.IsPAPIEnabled() {
   254  			if apiClient.apiClient.IsEnrolled() {
   255  				log.Info("Machine is enrolled in the console, Loading PAPI Client")
   256  
   257  				papiClient, err = NewPAPI(apiClient, dbClient, config.ConsoleConfig, *config.PapiLogLevel)
   258  				if err != nil {
   259  					return nil, err
   260  				}
   261  
   262  				controller.DecisionDeleteChan = papiClient.Channels.DeleteDecisionChannel
   263  			} else {
   264  				log.Error("Machine is not enrolled in the console, can't synchronize with the console")
   265  			}
   266  		}
   267  	}
   268  
   269  	trustedIPs, err := config.GetTrustedIPs()
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	controller.TrustedIPs = trustedIPs
   275  
   276  	return &APIServer{
   277  		URL:            config.ListenURI,
   278  		UnixSocket:     config.ListenSocket,
   279  		TLS:            config.TLS,
   280  		logFile:        logFile,
   281  		dbClient:       dbClient,
   282  		controller:     controller,
   283  		flushScheduler: flushScheduler,
   284  		router:         router,
   285  		apic:           apiClient,
   286  		papi:           papiClient,
   287  		httpServerTomb: tomb.Tomb{},
   288  		consoleConfig:  config.ConsoleConfig,
   289  	}, nil
   290  }
   291  
   292  func (s *APIServer) Router() (*gin.Engine, error) {
   293  	return s.router, nil
   294  }
   295  
   296  func (s *APIServer) Run(apiReady chan bool) error {
   297  	defer trace.CatchPanic("lapi/runServer")
   298  
   299  	tlsCfg, err := s.TLS.GetTLSConfig()
   300  	if err != nil {
   301  		return fmt.Errorf("while creating TLS config: %w", err)
   302  	}
   303  
   304  	s.httpServer = &http.Server{
   305  		Addr:      s.URL,
   306  		Handler:   s.router,
   307  		TLSConfig: tlsCfg,
   308  	}
   309  
   310  	if s.apic != nil {
   311  		s.apic.pushTomb.Go(func() error {
   312  			if err := s.apic.Push(); err != nil {
   313  				log.Errorf("capi push: %s", err)
   314  				return err
   315  			}
   316  
   317  			return nil
   318  		})
   319  
   320  		s.apic.pullTomb.Go(func() error {
   321  			if err := s.apic.Pull(); err != nil {
   322  				log.Errorf("capi pull: %s", err)
   323  				return err
   324  			}
   325  
   326  			return nil
   327  		})
   328  
   329  		// csConfig.API.Server.ConsoleConfig.ShareCustomScenarios
   330  		if s.apic.apiClient.IsEnrolled() {
   331  			if s.consoleConfig.IsPAPIEnabled() {
   332  				if s.papi.URL != "" {
   333  					log.Info("Starting PAPI decision receiver")
   334  					s.papi.pullTomb.Go(func() error {
   335  						if err := s.papi.Pull(); err != nil {
   336  							log.Errorf("papi pull: %s", err)
   337  							return err
   338  						}
   339  
   340  						return nil
   341  					})
   342  
   343  					s.papi.syncTomb.Go(func() error {
   344  						if err := s.papi.SyncDecisions(); err != nil {
   345  							log.Errorf("capi decisions sync: %s", err)
   346  							return err
   347  						}
   348  
   349  						return nil
   350  					})
   351  				} else {
   352  					log.Warnf("papi_url is not set in online_api_credentials.yaml, can't synchronize with the console. Run cscli console enable console_management to add it.")
   353  				}
   354  			} else {
   355  				log.Warningf("Machine is not allowed to synchronize decisions, you can enable it with `cscli console enable console_management`")
   356  			}
   357  		}
   358  
   359  		s.apic.metricsTomb.Go(func() error {
   360  			s.apic.SendMetrics(make(chan bool))
   361  			return nil
   362  		})
   363  	}
   364  
   365  	s.httpServerTomb.Go(func() error {
   366  		return s.listenAndServeLAPI(apiReady)
   367  	})
   368  
   369  	if err := s.httpServerTomb.Wait(); err != nil {
   370  		return fmt.Errorf("local API server stopped with error: %w", err)
   371          }
   372  
   373  	return nil
   374  }
   375  
   376  // listenAndServeLAPI starts the http server and blocks until it's closed
   377  // it also updates the URL field with the actual address the server is listening on
   378  // it's meant to be run in a separate goroutine
   379  func (s *APIServer) listenAndServeLAPI(apiReady chan bool) error {
   380  	var (
   381  		tcpListener    net.Listener
   382  		unixListener   net.Listener
   383  		err            error
   384  		serverError    = make(chan error, 2)
   385  		listenerClosed = make(chan struct{})
   386  	)
   387  
   388  	startServer := func(listener net.Listener, canTLS bool) {
   389  		if canTLS && s.TLS != nil && (s.TLS.CertFilePath != "" || s.TLS.KeyFilePath != "") {
   390  			if s.TLS.KeyFilePath == "" {
   391  				serverError <- errors.New("missing TLS key file")
   392  				return
   393  			}
   394  
   395  			if s.TLS.CertFilePath == "" {
   396  				serverError <- errors.New("missing TLS cert file")
   397  				return
   398  			}
   399  
   400  			err = s.httpServer.ServeTLS(listener, s.TLS.CertFilePath, s.TLS.KeyFilePath)
   401  		} else {
   402  			err = s.httpServer.Serve(listener)
   403  		}
   404  
   405  		switch {
   406  		case errors.Is(err, http.ErrServerClosed):
   407  			break
   408  		case err != nil:
   409  			serverError <- err
   410  		}
   411  	}
   412  
   413  	// Starting TCP listener
   414  	go func() {
   415  		if s.URL == "" {
   416  			return
   417  		}
   418  
   419  		tcpListener, err = net.Listen("tcp", s.URL)
   420  		if err != nil {
   421  			serverError <- fmt.Errorf("listening on %s: %w", s.URL, err)
   422  			return
   423  		}
   424  
   425  		log.Infof("CrowdSec Local API listening on %s", s.URL)
   426  		startServer(tcpListener, true)
   427  	}()
   428  
   429  	// Starting Unix socket listener
   430  	go func() {
   431  		if s.UnixSocket == "" {
   432  			return
   433  		}
   434  
   435  		_ = os.RemoveAll(s.UnixSocket)
   436  
   437  		unixListener, err = net.Listen("unix", s.UnixSocket)
   438  		if err != nil {
   439  			serverError <- fmt.Errorf("while creating unix listener: %w", err)
   440  			return
   441  		}
   442  
   443  		log.Infof("CrowdSec Local API listening on Unix socket %s", s.UnixSocket)
   444  		startServer(unixListener, false)
   445  	}()
   446  
   447  	apiReady <- true
   448  
   449  	select {
   450  	case err := <-serverError:
   451  		return err
   452  	case <-s.httpServerTomb.Dying():
   453  		log.Info("Shutting down API server")
   454  
   455  		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   456  		defer cancel()
   457  
   458  		if err := s.httpServer.Shutdown(ctx); err != nil {
   459  			log.Errorf("while shutting down http server: %v", err)
   460  		}
   461  
   462  		close(listenerClosed)
   463  	case <-listenerClosed:
   464  		if s.UnixSocket != "" {
   465  			_ = os.RemoveAll(s.UnixSocket)
   466  		}
   467  	}
   468  
   469  	return nil
   470  }
   471  
   472  func (s *APIServer) Close() {
   473  	if s.apic != nil {
   474  		s.apic.Shutdown() // stop apic first since it use dbClient
   475  	}
   476  
   477  	if s.papi != nil {
   478  		s.papi.Shutdown() // papi also uses the dbClient
   479  	}
   480  
   481  	s.dbClient.Ent.Close()
   482  
   483  	if s.flushScheduler != nil {
   484  		s.flushScheduler.Stop()
   485  	}
   486  }
   487  
   488  func (s *APIServer) Shutdown() error {
   489  	s.Close()
   490  
   491  	if s.httpServer != nil {
   492  		if err := s.httpServer.Shutdown(context.TODO()); err != nil {
   493  			return err
   494  		}
   495  	}
   496  
   497  	// close io.writer logger given to gin
   498  	if pipe, ok := gin.DefaultErrorWriter.(*io.PipeWriter); ok {
   499  		pipe.Close()
   500  	}
   501  
   502  	if pipe, ok := gin.DefaultWriter.(*io.PipeWriter); ok {
   503  		pipe.Close()
   504  	}
   505  
   506  	s.httpServerTomb.Kill(nil)
   507  
   508  	if err := s.httpServerTomb.Wait(); err != nil {
   509  		return fmt.Errorf("while waiting on httpServerTomb: %w", err)
   510  	}
   511  
   512  	return nil
   513  }
   514  
   515  func (s *APIServer) AttachPluginBroker(broker *csplugin.PluginBroker) {
   516  	s.controller.PluginChannel = broker.PluginChannel
   517  }
   518  
   519  func (s *APIServer) InitController() error {
   520  	err := s.controller.Init()
   521  	if err != nil {
   522  		return fmt.Errorf("controller init: %w", err)
   523  	}
   524  
   525  	if s.TLS == nil {
   526  		return nil
   527  	}
   528  
   529  	// TLS is configured: create the TLSAuth middleware for agents and bouncers
   530  
   531  	cacheExpiration := time.Hour
   532  	if s.TLS.CacheExpiration != nil {
   533  		cacheExpiration = *s.TLS.CacheExpiration
   534  	}
   535  
   536  	s.controller.HandlerV1.Middlewares.JWT.TlsAuth, err = v1.NewTLSAuth(s.TLS.AllowedAgentsOU, s.TLS.CRLPath,
   537  		cacheExpiration,
   538  		log.WithFields(log.Fields{
   539  			"component": "tls-auth",
   540  			"type":      "agent",
   541  		}))
   542  	if err != nil {
   543  		return fmt.Errorf("while creating TLS auth for agents: %w", err)
   544  	}
   545  
   546  	s.controller.HandlerV1.Middlewares.APIKey.TlsAuth, err = v1.NewTLSAuth(s.TLS.AllowedBouncersOU, s.TLS.CRLPath,
   547  		cacheExpiration,
   548  		log.WithFields(log.Fields{
   549  			"component": "tls-auth",
   550  			"type":      "bouncer",
   551  		}))
   552  	if err != nil {
   553  		return fmt.Errorf("while creating TLS auth for bouncers: %w", err)
   554  	}
   555  
   556  	return nil
   557  }