2020-11-30 09:37:17 +00:00
package apiserver
import (
"context"
2022-06-08 14:05:52 +00:00
"crypto/tls"
"crypto/x509"
2020-11-30 09:37:17 +00:00
"fmt"
2021-07-30 09:41:17 +00:00
"io"
2021-04-07 12:31:03 +00:00
"net"
2020-11-30 09:37:17 +00:00
"net/http"
2021-04-07 12:31:03 +00:00
"os"
"strings"
2020-11-30 09:37:17 +00:00
"time"
2023-01-31 13:47:44 +00:00
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
2020-11-30 09:37:17 +00:00
"github.com/crowdsecurity/crowdsec/pkg/apiserver/controllers"
2022-06-08 14:05:52 +00:00
v1 "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
2020-11-30 09:37:17 +00:00
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
2021-08-25 09:43:29 +00:00
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
2020-11-30 09:37:17 +00:00
"github.com/crowdsecurity/crowdsec/pkg/database"
2023-01-31 13:47:44 +00:00
"github.com/crowdsecurity/crowdsec/pkg/fflag"
2020-11-30 09:37:17 +00:00
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/gin-gonic/gin"
"github.com/go-co-op/gocron"
2023-01-31 13:47:44 +00:00
"github.com/golang-jwt/jwt/v4"
2020-11-30 09:37:17 +00:00
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
2021-04-07 09:39:24 +00:00
"gopkg.in/natefinch/lumberjack.v2"
2020-11-30 09:37:17 +00:00
"gopkg.in/tomb.v2"
)
var (
keyLength = 32
)
type APIServer struct {
URL string
TLS * csconfig . TLSCfg
dbClient * database . Client
logFile string
controller * controllers . Controller
flushScheduler * gocron . Scheduler
router * gin . Engine
httpServer * http . Server
apic * apic
2023-01-31 13:47:44 +00:00
papi * Papi
2020-11-30 09:37:17 +00:00
httpServerTomb tomb . Tomb
2022-01-13 15:46:16 +00:00
consoleConfig * csconfig . ConsoleConfig
2023-01-31 13:47:44 +00:00
isEnrolled bool
2020-11-30 09:37:17 +00:00
}
2021-04-07 12:31:03 +00:00
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func CustomRecoveryWithWriter ( ) gin . HandlerFunc {
return func ( c * gin . Context ) {
defer func ( ) {
if err := recover ( ) ; err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne , ok := err . ( * net . OpError ) ; ok {
if se , ok := ne . Err . ( * os . SyscallError ) ; ok {
if strings . Contains ( strings . ToLower ( se . Error ( ) ) , "broken pipe" ) || strings . Contains ( strings . ToLower ( se . Error ( ) ) , "connection reset by peer" ) {
brokenPipe = true
}
}
}
// because of https://github.com/golang/net/blob/39120d07d75e76f0079fe5d27480bcb965a21e4c/http2/server.go
// and because it seems gin doesn't handle those neither, we need to "hand define" some errors to properly catch them
if strErr , ok := err . ( error ) ; ok {
//stolen from http2/server.go in x/net
var (
errClientDisconnected = errors . New ( "client disconnected" )
errClosedBody = errors . New ( "body closed by handler" )
errHandlerComplete = errors . New ( "http2: request body closed due to handler exiting" )
errStreamClosed = errors . New ( "http2: stream closed" )
)
2022-11-29 08:16:07 +00:00
if errors . Is ( strErr , errClientDisconnected ) ||
errors . Is ( strErr , errClosedBody ) ||
errors . Is ( strErr , errHandlerComplete ) ||
errors . Is ( strErr , errStreamClosed ) {
2021-04-07 12:31:03 +00:00
brokenPipe = true
}
}
if brokenPipe {
log . Warningf ( "client %s disconnected : %s" , c . ClientIP ( ) , err )
c . Abort ( )
} else {
filename := types . WriteStackTrace ( err )
log . Warningf ( "client %s error : %s" , c . ClientIP ( ) , err )
log . Warningf ( "stacktrace written to %s, please join to your issue" , filename )
c . AbortWithStatus ( http . StatusInternalServerError )
}
}
} ( )
c . Next ( )
}
}
2020-11-30 09:37:17 +00:00
func NewServer ( config * csconfig . LocalApiServerCfg ) ( * APIServer , error ) {
var flushScheduler * gocron . Scheduler
dbClient , err := database . NewClient ( config . DbConfig )
if err != nil {
2022-05-19 08:48:08 +00:00
return & APIServer { } , errors . Wrap ( err , "unable to init database client" )
2020-11-30 09:37:17 +00:00
}
if config . DbConfig . Flush != nil {
flushScheduler , err = dbClient . StartFlushScheduler ( config . DbConfig . Flush )
if err != nil {
return & APIServer { } , err
}
}
logFile := ""
2021-03-22 16:46:55 +00:00
if config . LogMedia == "file" {
2020-11-30 09:37:17 +00:00
logFile = fmt . Sprintf ( "%s/crowdsec_api.log" , config . LogDir )
}
if log . GetLevel ( ) < log . DebugLevel {
gin . SetMode ( gin . ReleaseMode )
}
log . Debugf ( "starting router, logging to %s" , logFile )
router := gin . New ( )
2022-01-17 16:18:12 +00:00
if config . TrustedProxies != nil && config . UseForwardedForHeaders {
if err := router . SetTrustedProxies ( * config . TrustedProxies ) ; err != nil {
return & APIServer { } , errors . Wrap ( err , "while setting trusted_proxies" )
}
router . ForwardedByClientIP = true
} else {
router . ForwardedByClientIP = false
}
2020-11-30 09:37:17 +00:00
/*The logger that will be used by handlers*/
clog := log . New ( )
2021-04-07 09:39:24 +00:00
2020-11-30 09:37:17 +00:00
if err := types . ConfigureLogger ( clog ) ; err != nil {
return nil , errors . Wrap ( err , "while configuring gin logger" )
}
if config . LogLevel != nil {
clog . SetLevel ( * config . LogLevel )
}
2021-04-07 09:39:24 +00:00
/*Configure logs*/
2020-11-30 09:37:17 +00:00
if logFile != "" {
2022-01-18 15:54:02 +00:00
_maxsize := 500
if config . LogMaxSize != 0 {
_maxsize = config . LogMaxSize
}
_maxfiles := 3
if config . LogMaxFiles != 0 {
_maxfiles = config . LogMaxFiles
}
_maxage := 28
if config . LogMaxAge != 0 {
_maxage = config . LogMaxAge
}
_compress := true
if config . CompressLogs != nil {
_compress = * config . CompressLogs
}
/ * cf . https : //github.com/natefinch/lumberjack/issues/82
let ' s create the file beforehand w / the right perms * /
// check if file exists
_ , err := os . Stat ( logFile )
// create file if not exists, purposefully ignore errors
if os . IsNotExist ( err ) {
file , _ := os . OpenFile ( logFile , os . O_RDWR | os . O_CREATE , 0600 )
file . Close ( )
}
2021-04-07 09:39:24 +00:00
LogOutput := & lumberjack . Logger {
Filename : logFile ,
2022-01-18 15:54:02 +00:00
MaxSize : _maxsize , //megabytes
MaxBackups : _maxfiles ,
MaxAge : _maxage , //days
Compress : _compress , //disabled by default
2020-11-30 09:37:17 +00:00
}
2021-04-07 09:39:24 +00:00
clog . SetOutput ( LogOutput )
2020-11-30 09:37:17 +00:00
}
2021-04-07 09:39:24 +00:00
gin . DefaultErrorWriter = clog . WriterLevel ( log . ErrorLevel )
gin . DefaultWriter = clog . Writer ( )
2020-11-30 09:37:17 +00:00
router . Use ( gin . LoggerWithFormatter ( func ( param gin . LogFormatterParams ) string {
return fmt . Sprintf ( "%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n" ,
param . ClientIP ,
param . TimeStamp . Format ( time . RFC1123 ) ,
param . Method ,
param . Path ,
param . Request . Proto ,
param . StatusCode ,
param . Latency ,
param . Request . UserAgent ( ) ,
param . ErrorMessage ,
)
} ) )
router . NoRoute ( func ( c * gin . Context ) {
c . JSON ( http . StatusNotFound , gin . H { "message" : "Page or Method not found" } )
} )
2021-04-07 12:31:03 +00:00
router . Use ( CustomRecoveryWithWriter ( ) )
2022-01-13 15:46:16 +00:00
2020-11-30 09:37:17 +00:00
controller := & controllers . Controller {
2023-02-24 13:44:21 +00:00
DBClient : dbClient ,
Ectx : context . Background ( ) ,
Router : router ,
Profiles : config . Profiles ,
Log : clog ,
ConsoleConfig : config . ConsoleConfig ,
DisableRemoteLapiRegistration : config . DisableRemoteLapiRegistration ,
2020-11-30 09:37:17 +00:00
}
var apiClient * apic
2023-01-31 13:47:44 +00:00
var papiClient * Papi
var isMachineEnrolled = false
2020-11-30 09:37:17 +00:00
2021-03-02 08:25:12 +00:00
if config . OnlineClient != nil && config . OnlineClient . Credentials != nil {
2023-01-31 13:47:44 +00:00
log . Printf ( "Loading CAPI manager" )
2023-03-21 10:50:10 +00:00
apiClient , err = NewAPIC ( config . OnlineClient , dbClient , config . ConsoleConfig , config . CapiWhitelists )
2020-11-30 09:37:17 +00:00
if err != nil {
return & APIServer { } , err
}
2023-01-31 13:47:44 +00:00
log . Infof ( "CAPI manager configured successfully" )
isMachineEnrolled = isEnrolled ( apiClient . apiClient )
2023-02-08 09:35:21 +00:00
controller . AlertsAddChan = apiClient . AlertsAddChan
if fflag . PapiClient . IsEnabled ( ) {
if isMachineEnrolled {
log . Infof ( "Machine is enrolled in the console, Loading PAPI Client" )
papiClient , err = NewPAPI ( apiClient , dbClient , config . ConsoleConfig , * config . PapiLogLevel )
if err != nil {
return & APIServer { } , err
}
controller . DecisionDeleteChan = papiClient . Channels . DeleteDecisionChannel
} else {
log . Errorf ( "Machine is not enrolled in the console, can't synchronize with the console" )
2023-01-31 13:47:44 +00:00
}
}
2020-11-30 09:37:17 +00:00
} else {
apiClient = nil
2023-01-31 13:47:44 +00:00
controller . AlertsAddChan = nil
controller . DecisionDeleteChan = nil
2020-11-30 09:37:17 +00:00
}
2023-01-31 13:47:44 +00:00
2022-03-16 16:28:34 +00:00
if trustedIPs , err := config . GetTrustedIPs ( ) ; err == nil {
controller . TrustedIPs = trustedIPs
} else {
return & APIServer { } , err
}
2020-11-30 09:37:17 +00:00
return & APIServer {
URL : config . ListenURI ,
TLS : config . TLS ,
logFile : logFile ,
dbClient : dbClient ,
controller : controller ,
flushScheduler : flushScheduler ,
router : router ,
apic : apiClient ,
2023-01-31 13:47:44 +00:00
papi : papiClient ,
2020-11-30 09:37:17 +00:00
httpServerTomb : tomb . Tomb { } ,
2022-01-13 15:46:16 +00:00
consoleConfig : config . ConsoleConfig ,
2023-01-31 13:47:44 +00:00
isEnrolled : isMachineEnrolled ,
2020-11-30 09:37:17 +00:00
} , nil
}
2023-01-31 13:47:44 +00:00
func isEnrolled ( client * apiclient . ApiClient ) bool {
apiHTTPClient := client . GetClient ( )
jwtTransport := apiHTTPClient . Transport . ( * apiclient . JWTTransport )
tokenStr := jwtTransport . Token
token , _ := jwt . Parse ( tokenStr , nil )
if token == nil {
return false
}
claims := token . Claims . ( jwt . MapClaims )
_ , ok := claims [ "organization_id" ]
return ok
}
2020-11-30 09:37:17 +00:00
func ( s * APIServer ) Router ( ) ( * gin . Engine , error ) {
return s . router , nil
}
2022-06-08 14:05:52 +00:00
func ( s * APIServer ) GetTLSConfig ( ) ( * tls . Config , error ) {
var caCert [ ] byte
var err error
var caCertPool * x509 . CertPool
var clientAuthType tls . ClientAuthType
if s . TLS == nil {
return & tls . Config { } , nil
}
if s . TLS . ClientVerification == "" {
//sounds like a sane default : verify client cert if given, but don't make it mandatory
clientAuthType = tls . VerifyClientCertIfGiven
} else {
clientAuthType , err = getTLSAuthType ( s . TLS . ClientVerification )
if err != nil {
return nil , err
}
}
if s . TLS . CACertPath != "" {
if clientAuthType > tls . RequestClientCert {
log . Infof ( "(tls) Client Auth Type set to %s" , clientAuthType . String ( ) )
2022-09-06 11:55:03 +00:00
caCert , err = os . ReadFile ( s . TLS . CACertPath )
2022-06-08 14:05:52 +00:00
if err != nil {
return nil , errors . Wrap ( err , "Error opening cert file" )
}
caCertPool = x509 . NewCertPool ( )
caCertPool . AppendCertsFromPEM ( caCert )
}
}
return & tls . Config {
ServerName : s . TLS . ServerName , //should it be removed ?
ClientAuth : clientAuthType ,
ClientCAs : caCertPool ,
MinVersion : tls . VersionTLS12 , // TLS versions below 1.2 are considered insecure - see https://www.rfc-editor.org/rfc/rfc7525.txt for details
} , nil
}
2022-06-24 13:55:21 +00:00
func ( s * APIServer ) Run ( apiReady chan bool ) error {
2020-11-30 09:37:17 +00:00
defer types . CatchPanic ( "lapi/runServer" )
2022-06-08 14:05:52 +00:00
tlsCfg , err := s . GetTLSConfig ( )
if err != nil {
return errors . Wrap ( err , "while creating TLS config" )
}
2020-11-30 09:37:17 +00:00
s . httpServer = & http . Server {
2022-06-08 14:05:52 +00:00
Addr : s . URL ,
Handler : s . router ,
TLSConfig : tlsCfg ,
2020-11-30 09:37:17 +00:00
}
if s . apic != nil {
s . apic . pushTomb . Go ( func ( ) error {
if err := s . apic . Push ( ) ; err != nil {
log . Errorf ( "capi push: %s" , err )
return err
}
return nil
} )
2023-01-31 13:47:44 +00:00
2020-11-30 09:37:17 +00:00
s . apic . pullTomb . Go ( func ( ) error {
if err := s . apic . Pull ( ) ; err != nil {
log . Errorf ( "capi pull: %s" , err )
return err
}
return nil
} )
2023-01-31 13:47:44 +00:00
//csConfig.API.Server.ConsoleConfig.ShareCustomScenarios
if s . isEnrolled {
if fflag . PapiClient . IsEnabled ( ) {
2023-03-06 14:38:58 +00:00
if s . consoleConfig . ConsoleManagement != nil && * s . consoleConfig . ConsoleManagement {
if s . papi . URL != "" {
log . Infof ( "Starting PAPI decision receiver" )
s . papi . pullTomb . Go ( func ( ) error {
if err := s . papi . Pull ( ) ; err != nil {
log . Errorf ( "papi pull: %s" , err )
return err
}
return nil
} )
s . papi . syncTomb . Go ( func ( ) error {
if err := s . papi . SyncDecisions ( ) ; err != nil {
log . Errorf ( "capi decisions sync: %s" , err )
return err
}
return nil
} )
} else {
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." )
}
2023-01-31 13:47:44 +00:00
} else {
log . Warningf ( "Machine is not allowed to synchronize decisions, you can enable it with `cscli console enable console_management`" )
}
}
}
2020-11-30 09:37:17 +00:00
s . apic . metricsTomb . Go ( func ( ) error {
2022-09-30 14:01:42 +00:00
s . apic . SendMetrics ( make ( chan bool ) )
2020-11-30 09:37:17 +00:00
return nil
} )
}
s . httpServerTomb . Go ( func ( ) error {
go func ( ) {
2022-06-24 13:55:21 +00:00
apiReady <- true
2022-07-01 14:56:13 +00:00
log . Infof ( "CrowdSec Local API listening on %s" , s . URL )
2023-01-26 16:12:59 +00:00
if s . TLS != nil && ( s . TLS . CertFilePath != "" || s . TLS . KeyFilePath != "" ) {
if s . TLS . KeyFilePath == "" {
log . Fatalf ( "while serving local API: %v" , errors . New ( "missing TLS key file" ) )
} else if s . TLS . CertFilePath == "" {
log . Fatalf ( "while serving local API: %v" , errors . New ( "missing TLS cert file" ) )
}
2020-11-30 09:37:17 +00:00
if err := s . httpServer . ListenAndServeTLS ( s . TLS . CertFilePath , s . TLS . KeyFilePath ) ; err != nil {
2022-11-28 09:35:12 +00:00
log . Fatalf ( "while serving local API: %v" , err )
2020-11-30 09:37:17 +00:00
}
} else {
if err := s . httpServer . ListenAndServe ( ) ; err != http . ErrServerClosed {
2022-11-28 09:35:12 +00:00
log . Fatalf ( "while serving local API: %v" , err )
2020-11-30 09:37:17 +00:00
}
}
} ( )
<- s . httpServerTomb . Dying ( )
return nil
} )
return nil
}
func ( s * APIServer ) Close ( ) {
if s . apic != nil {
s . apic . Shutdown ( ) // stop apic first since it use dbClient
}
s . dbClient . Ent . Close ( )
if s . flushScheduler != nil {
s . flushScheduler . Stop ( )
}
}
func ( s * APIServer ) Shutdown ( ) error {
s . Close ( )
2022-11-28 10:55:08 +00:00
if s . httpServer != nil {
if err := s . httpServer . Shutdown ( context . TODO ( ) ) ; err != nil {
return err
}
2020-11-30 09:37:17 +00:00
}
2021-07-30 09:41:17 +00:00
//close io.writer logger given to gin
if pipe , ok := gin . DefaultErrorWriter . ( * io . PipeWriter ) ; ok {
pipe . Close ( )
}
if pipe , ok := gin . DefaultWriter . ( * io . PipeWriter ) ; ok {
pipe . Close ( )
}
s . httpServerTomb . Kill ( nil )
if err := s . httpServerTomb . Wait ( ) ; err != nil {
return errors . Wrap ( err , "while waiting on httpServerTomb" )
}
2020-11-30 09:37:17 +00:00
return nil
}
2021-08-25 09:43:29 +00:00
func ( s * APIServer ) AttachPluginBroker ( broker * csplugin . PluginBroker ) {
s . controller . PluginChannel = broker . PluginChannel
}
func ( s * APIServer ) InitController ( ) error {
2022-06-08 14:05:52 +00:00
2021-08-25 09:43:29 +00:00
err := s . controller . Init ( )
2022-06-08 14:05:52 +00:00
if err != nil {
return errors . Wrap ( err , "controller init" )
}
if s . TLS != nil {
var cacheExpiration time . Duration
if s . TLS . CacheExpiration != nil {
cacheExpiration = * s . TLS . CacheExpiration
} else {
cacheExpiration = time . Hour
}
s . controller . HandlerV1 . Middlewares . JWT . TlsAuth , err = v1 . NewTLSAuth ( s . TLS . AllowedAgentsOU , s . TLS . CRLPath ,
cacheExpiration ,
log . WithFields ( log . Fields {
"component" : "tls-auth" ,
"type" : "agent" ,
} ) )
if err != nil {
return errors . Wrap ( err , "while creating TLS auth for agents" )
}
s . controller . HandlerV1 . Middlewares . APIKey . TlsAuth , err = v1 . NewTLSAuth ( s . TLS . AllowedBouncersOU , s . TLS . CRLPath ,
cacheExpiration ,
log . WithFields ( log . Fields {
"component" : "tls-auth" ,
"type" : "bouncer" ,
} ) )
if err != nil {
return errors . Wrap ( err , "while creating TLS auth for bouncers" )
}
}
2021-08-25 09:43:29 +00:00
return err
}