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 }