2020-11-30 09:37:17 +00:00
package apiserver
import (
"context"
2024-01-15 10:44:38 +00:00
"errors"
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"
2023-06-07 10:58:35 +00:00
"path/filepath"
2021-04-07 12:31:03 +00:00
"strings"
2020-11-30 09:37:17 +00:00
"time"
2023-06-07 10:58:35 +00:00
"github.com/gin-gonic/gin"
"github.com/go-co-op/gocron"
log "github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
"gopkg.in/tomb.v2"
2023-07-28 14:35:08 +00:00
"github.com/crowdsecurity/go-cs-lib/trace"
2023-05-23 08:52:47 +00:00
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"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
2023-12-14 13:54:11 +00:00
const keyLength = 32
2020-11-30 09:37:17 +00:00
type APIServer struct {
URL string
2024-03-14 09:43:02 +00:00
UnixSocket string
2020-11-30 09:37:17 +00:00
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
2020-11-30 09:37:17 +00:00
}
2023-12-14 13:54:11 +00:00
func recoverFromPanic ( c * gin . Context ) {
err := recover ( )
if err == nil {
return
}
2021-04-07 12:31:03 +00:00
2023-12-14 13:54:11 +00:00
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
brokenPipe := false
2021-04-07 12:31:03 +00:00
2023-12-14 13:54:11 +00:00
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
2021-04-07 12:31:03 +00:00
}
2023-12-14 13:54:11 +00:00
}
}
// 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 {
2024-03-14 09:43:02 +00:00
// stolen from http2/server.go in x/net
2023-12-14 13:54:11 +00:00
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" )
)
if errors . Is ( strErr , errClientDisconnected ) ||
errors . Is ( strErr , errClosedBody ) ||
errors . Is ( strErr , errHandlerComplete ) ||
errors . Is ( strErr , errStreamClosed ) {
brokenPipe = true
}
}
if brokenPipe {
2024-04-22 21:54:51 +00:00
log . Warningf ( "client %s disconnected: %s" , c . ClientIP ( ) , err )
2023-12-14 13:54:11 +00:00
c . Abort ( )
} else {
2024-04-22 21:54:51 +00:00
log . Warningf ( "client %s error: %s" , c . ClientIP ( ) , err )
filename , err := trace . WriteStackTrace ( err )
if err != nil {
log . Errorf ( "also while writing stacktrace: %s" , err )
}
2023-12-14 13:54:11 +00:00
log . Warningf ( "stacktrace written to %s, please join to your issue" , filename )
c . AbortWithStatus ( http . StatusInternalServerError )
}
}
// CustomRecoveryWithWriter returns a middleware for a writer that recovers from any panics and writes a 500 if there was one.
func CustomRecoveryWithWriter ( ) gin . HandlerFunc {
return func ( c * gin . Context ) {
defer recoverFromPanic ( c )
2021-04-07 12:31:03 +00:00
c . Next ( )
}
}
2023-12-14 13:54:11 +00:00
// XXX: could be a method of LocalApiServerCfg
func newGinLogger ( config * csconfig . LocalApiServerCfg ) ( * log . Logger , string , error ) {
clog := log . New ( )
if err := types . ConfigureLogger ( clog ) ; err != nil {
return nil , "" , fmt . Errorf ( "while configuring gin logger: %w" , err )
}
if config . LogLevel != nil {
clog . SetLevel ( * config . LogLevel )
}
if config . LogMedia != "file" {
return clog , "" , nil
}
// Log rotation
logFile := filepath . Join ( config . LogDir , "crowdsec_api.log" )
log . Debugf ( "starting router, logging to %s" , logFile )
logger := & lumberjack . Logger {
Filename : logFile ,
2024-03-14 09:43:02 +00:00
MaxSize : 500 , // megabytes
2023-12-14 13:54:11 +00:00
MaxBackups : 3 ,
2024-03-14 09:43:02 +00:00
MaxAge : 28 , // days
Compress : true , // disabled by default
2023-12-14 13:54:11 +00:00
}
if config . LogMaxSize != 0 {
logger . MaxSize = config . LogMaxSize
}
if config . LogMaxFiles != 0 {
logger . MaxBackups = config . LogMaxFiles
}
if config . LogMaxAge != 0 {
logger . MaxAge = config . LogMaxAge
}
if config . CompressLogs != nil {
logger . Compress = * config . CompressLogs
}
clog . SetOutput ( logger )
return clog , logFile , nil
}
// NewServer creates a LAPI server.
// It sets up a gin router, a database client, and a controller.
2020-11-30 09:37:17 +00:00
func NewServer ( config * csconfig . LocalApiServerCfg ) ( * APIServer , error ) {
var flushScheduler * gocron . Scheduler
2023-12-14 13:54:11 +00:00
2020-11-30 09:37:17 +00:00
dbClient , err := database . NewClient ( config . DbConfig )
if err != nil {
2023-12-04 22:06:01 +00:00
return nil , fmt . Errorf ( "unable to init database client: %w" , err )
2020-11-30 09:37:17 +00:00
}
if config . DbConfig . Flush != nil {
flushScheduler , err = dbClient . StartFlushScheduler ( config . DbConfig . Flush )
if err != nil {
2023-12-04 22:06:01 +00:00
return nil , err
2020-11-30 09:37:17 +00:00
}
}
if log . GetLevel ( ) < log . DebugLevel {
gin . SetMode ( gin . ReleaseMode )
}
2023-12-14 13:54:11 +00:00
2020-11-30 09:37:17 +00:00
router := gin . New ( )
2022-01-17 16:18:12 +00:00
2023-12-14 13:54:11 +00:00
router . ForwardedByClientIP = false
2024-03-14 09:43:02 +00:00
// set the remore address of the request to 127.0.0.1 if it comes from a unix socket
router . Use ( func ( c * gin . Context ) {
if c . Request . RemoteAddr == "@" {
c . Request . RemoteAddr = "127.0.0.1:65535"
}
} )
2022-01-17 16:18:12 +00:00
if config . TrustedProxies != nil && config . UseForwardedForHeaders {
2023-12-14 13:54:11 +00:00
if err = router . SetTrustedProxies ( * config . TrustedProxies ) ; err != nil {
2023-12-04 22:06:01 +00:00
return nil , fmt . Errorf ( "while setting trusted_proxies: %w" , err )
2022-01-17 16:18:12 +00:00
}
2020-11-30 09:37:17 +00:00
2023-12-14 13:54:11 +00:00
router . ForwardedByClientIP = true
2020-11-30 09:37:17 +00:00
}
2023-12-14 13:54:11 +00:00
// The logger that will be used by handlers
clog , logFile , err := newGinLogger ( config )
if err != nil {
return nil , err
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
}
2023-12-14 13:54:11 +00:00
var (
2024-03-14 09:43:02 +00:00
apiClient * apic
papiClient * Papi
2023-12-14 13:54:11 +00:00
)
controller . AlertsAddChan = nil
controller . DecisionDeleteChan = nil
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-12-14 13:54:11 +00:00
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 {
2023-12-04 22:06:01 +00:00
return nil , err
2020-11-30 09:37:17 +00:00
}
2023-12-14 13:54:11 +00:00
2023-01-31 13:47:44 +00:00
log . Infof ( "CAPI manager configured successfully" )
2023-12-14 13:54:11 +00:00
2023-02-08 09:35:21 +00:00
controller . AlertsAddChan = apiClient . AlertsAddChan
2023-12-14 13:54:11 +00:00
2024-03-14 13:03:43 +00:00
if config . ConsoleConfig . IsPAPIEnabled ( ) {
if apiClient . apiClient . IsEnrolled ( ) {
2024-01-22 12:25:36 +00:00
log . Info ( "Machine is enrolled in the console, Loading PAPI Client" )
2024-01-16 08:16:21 +00:00
papiClient , err = NewPAPI ( apiClient , dbClient , config . ConsoleConfig , * config . PapiLogLevel )
if err != nil {
return nil , err
}
2023-12-14 13:54:11 +00:00
2024-01-16 08:16:21 +00:00
controller . DecisionDeleteChan = papiClient . Channels . DeleteDecisionChannel
2024-03-14 13:03:43 +00:00
} else {
log . Error ( "Machine is not enrolled in the console, can't synchronize with the console" )
2024-01-16 08:16:21 +00:00
}
2023-01-31 13:47:44 +00:00
}
2020-11-30 09:37:17 +00:00
}
2023-01-31 13:47:44 +00:00
2023-12-14 13:54:11 +00:00
trustedIPs , err := config . GetTrustedIPs ( )
if err != nil {
2023-12-04 22:06:01 +00:00
return nil , err
2022-03-16 16:28:34 +00:00
}
2020-11-30 09:37:17 +00:00
2023-12-14 13:54:11 +00:00
controller . TrustedIPs = trustedIPs
2020-11-30 09:37:17 +00:00
return & APIServer {
URL : config . ListenURI ,
2024-03-14 09:43:02 +00:00
UnixSocket : config . ListenSocket ,
2020-11-30 09:37:17 +00:00
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 ,
2020-11-30 09:37:17 +00:00
} , nil
2023-01-31 13:47:44 +00:00
}
2020-11-30 09:37:17 +00:00
func ( s * APIServer ) Router ( ) ( * gin . Engine , error ) {
return s . router , nil
}
2022-06-24 13:55:21 +00:00
func ( s * APIServer ) Run ( apiReady chan bool ) error {
2023-05-23 08:52:47 +00:00
defer trace . CatchPanic ( "lapi/runServer" )
2023-12-14 13:54:11 +00:00
tlsCfg , err := s . TLS . GetTLSConfig ( )
2022-06-08 14:05:52 +00:00
if err != nil {
2023-07-06 08:14:45 +00:00
return fmt . Errorf ( "while creating TLS config: %w" , err )
2022-06-08 14:05:52 +00:00
}
2023-12-14 13:54:11 +00:00
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
}
2024-01-10 11:00:22 +00:00
2020-11-30 09:37:17 +00:00
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
}
2024-01-10 11:00:22 +00:00
2020-11-30 09:37:17 +00:00
return nil
} )
2023-01-31 13:47:44 +00:00
2024-03-14 09:43:02 +00:00
// csConfig.API.Server.ConsoleConfig.ShareCustomScenarios
2024-01-16 08:14:33 +00:00
if s . apic . apiClient . IsEnrolled ( ) {
2024-01-16 08:16:21 +00:00
if s . consoleConfig . IsPAPIEnabled ( ) {
2023-12-08 13:55:45 +00:00
if s . papi . URL != "" {
2024-03-14 09:43:02 +00:00
log . Info ( "Starting PAPI decision receiver" )
2023-12-08 13:55:45 +00:00
s . papi . pullTomb . Go ( func ( ) error {
if err := s . papi . Pull ( ) ; err != nil {
log . Errorf ( "papi pull: %s" , err )
return err
}
2024-01-10 11:00:22 +00:00
2023-12-08 13:55:45 +00:00
return nil
} )
s . papi . syncTomb . Go ( func ( ) error {
if err := s . papi . SyncDecisions ( ) ; err != nil {
log . Errorf ( "capi decisions sync: %s" , err )
return err
}
2024-01-10 11:00:22 +00:00
2023-12-08 13:55:45 +00:00
return nil
} )
2023-01-31 13:47:44 +00:00
} else {
2023-12-08 13:55:45 +00:00
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
}
2023-12-08 13:55:45 +00:00
} else {
log . Warningf ( "Machine is not allowed to synchronize decisions, you can enable it with `cscli console enable console_management`" )
2023-01-31 13:47:44 +00:00
}
}
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
} )
}
2024-03-14 09:43:02 +00:00
s . httpServerTomb . Go ( func ( ) error {
return s . listenAndServeLAPI ( apiReady )
} )
if err := s . httpServerTomb . Wait ( ) ; err != nil {
return fmt . Errorf ( "local API server stopped with error: %w" , err )
}
2023-01-26 16:12:59 +00:00
2023-12-14 13:54:11 +00:00
return nil
}
2024-03-14 09:43:02 +00:00
// listenAndServeLAPI starts the http server and blocks until it's closed
2023-12-14 13:54:11 +00:00
// it also updates the URL field with the actual address the server is listening on
// it's meant to be run in a separate goroutine
2024-03-14 09:43:02 +00:00
func ( s * APIServer ) listenAndServeLAPI ( apiReady chan bool ) error {
var (
tcpListener net . Listener
unixListener net . Listener
err error
serverError = make ( chan error , 2 )
listenerClosed = make ( chan struct { } )
)
2023-12-14 13:54:11 +00:00
2024-03-14 09:43:02 +00:00
startServer := func ( listener net . Listener , canTLS bool ) {
if canTLS && s . TLS != nil && ( s . TLS . CertFilePath != "" || s . TLS . KeyFilePath != "" ) {
2023-12-14 13:54:11 +00:00
if s . TLS . KeyFilePath == "" {
serverError <- errors . New ( "missing TLS key file" )
return
2024-01-15 10:44:38 +00:00
}
if s . TLS . CertFilePath == "" {
2023-12-14 13:54:11 +00:00
serverError <- errors . New ( "missing TLS cert file" )
return
2020-11-30 09:37:17 +00:00
}
2023-12-14 13:54:11 +00:00
err = s . httpServer . ServeTLS ( listener , s . TLS . CertFilePath , s . TLS . KeyFilePath )
} else {
err = s . httpServer . Serve ( listener )
}
2024-03-14 09:43:02 +00:00
switch {
case errors . Is ( err , http . ErrServerClosed ) :
break
case err != nil :
serverError <- err
}
}
// Starting TCP listener
go func ( ) {
if s . URL == "" {
return
}
tcpListener , err = net . Listen ( "tcp" , s . URL )
if err != nil {
serverError <- fmt . Errorf ( "listening on %s: %w" , s . URL , err )
return
}
log . Infof ( "CrowdSec Local API listening on %s" , s . URL )
startServer ( tcpListener , true )
} ( )
// Starting Unix socket listener
go func ( ) {
if s . UnixSocket == "" {
return
}
_ = os . RemoveAll ( s . UnixSocket )
unixListener , err = net . Listen ( "unix" , s . UnixSocket )
if err != nil {
serverError <- fmt . Errorf ( "while creating unix listener: %w" , err )
2023-12-14 13:54:11 +00:00
return
}
2024-03-14 09:43:02 +00:00
log . Infof ( "CrowdSec Local API listening on Unix socket %s" , s . UnixSocket )
startServer ( unixListener , false )
2023-12-14 13:54:11 +00:00
} ( )
2024-03-14 09:43:02 +00:00
apiReady <- true
2023-12-14 13:54:11 +00:00
select {
case err := <- serverError :
2024-03-14 09:43:02 +00:00
return err
2023-12-14 13:54:11 +00:00
case <- s . httpServerTomb . Dying ( ) :
2024-03-14 09:43:02 +00:00
log . Info ( "Shutting down API server" )
2023-12-14 13:54:11 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Second )
defer cancel ( )
if err := s . httpServer . Shutdown ( ctx ) ; err != nil {
2024-03-14 09:43:02 +00:00
log . Errorf ( "while shutting down http server: %v" , err )
}
close ( listenerClosed )
case <- listenerClosed :
if s . UnixSocket != "" {
_ = os . RemoveAll ( s . UnixSocket )
2023-12-14 13:54:11 +00:00
}
}
2024-03-14 09:43:02 +00:00
return nil
2020-11-30 09:37:17 +00:00
}
func ( s * APIServer ) Close ( ) {
if s . apic != nil {
s . apic . Shutdown ( ) // stop apic first since it use dbClient
}
2023-12-14 13:54:11 +00:00
2023-03-21 13:06:19 +00:00
if s . papi != nil {
s . papi . Shutdown ( ) // papi also uses the dbClient
}
2023-12-14 13:54:11 +00:00
2020-11-30 09:37:17 +00:00
s . dbClient . Ent . Close ( )
2023-12-14 13:54:11 +00:00
2020-11-30 09:37:17 +00:00
if s . flushScheduler != nil {
s . flushScheduler . Stop ( )
}
}
func ( s * APIServer ) Shutdown ( ) error {
s . Close ( )
2023-12-14 13:54:11 +00:00
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
2024-03-14 09:43:02 +00:00
// close io.writer logger given to gin
2021-07-30 09:41:17 +00:00
if pipe , ok := gin . DefaultErrorWriter . ( * io . PipeWriter ) ; ok {
pipe . Close ( )
}
2023-12-14 13:54:11 +00:00
2021-07-30 09:41:17 +00:00
if pipe , ok := gin . DefaultWriter . ( * io . PipeWriter ) ; ok {
pipe . Close ( )
}
2023-12-14 13:54:11 +00:00
2021-07-30 09:41:17 +00:00
s . httpServerTomb . Kill ( nil )
2023-12-14 13:54:11 +00:00
2021-07-30 09:41:17 +00:00
if err := s . httpServerTomb . Wait ( ) ; err != nil {
2023-07-06 08:14:45 +00:00
return fmt . Errorf ( "while waiting on httpServerTomb: %w" , err )
2021-07-30 09:41:17 +00:00
}
2023-12-14 13:54:11 +00:00
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 {
err := s . controller . Init ( )
2022-06-08 14:05:52 +00:00
if err != nil {
2023-07-06 08:14:45 +00:00
return fmt . Errorf ( "controller init: %w" , err )
2022-06-08 14:05:52 +00:00
}
2023-12-14 13:54:11 +00:00
if s . TLS == nil {
return nil
}
// TLS is configured: create the TLSAuth middleware for agents and bouncers
cacheExpiration := time . Hour
if s . TLS . CacheExpiration != nil {
cacheExpiration = * s . TLS . CacheExpiration
}
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 fmt . Errorf ( "while creating TLS auth for agents: %w" , err )
2022-06-08 14:05:52 +00:00
}
2023-12-14 13:54:11 +00:00
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 fmt . Errorf ( "while creating TLS auth for bouncers: %w" , err )
}
return nil
2021-08-25 09:43:29 +00:00
}