-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: appsec lib #42
base: main
Are you sure you want to change the base?
feat: appsec lib #42
Changes from all commits
014747c
e8ac104
7e833a6
2bfb4df
25c0286
dc574b5
6412047
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
package csbouncer | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/tls" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
"strings" | ||
"time" | ||
|
||
"gopkg.in/yaml.v2" | ||
|
||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
const ( | ||
crowdsecAppsecIPHeader = "X-Crowdsec-Appsec-Ip" | ||
crowdsecAppsecURIHeader = "X-Crowdsec-Appsec-Uri" | ||
crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host" | ||
crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb" | ||
crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key" | ||
crowdsecAppsecUserAgent = "X-Crowdsec-Appsec-User-Agent" | ||
) | ||
|
||
type Timeout struct { | ||
ConnectTimeout *int `yaml:"connect_timeout"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably transform those to |
||
TLSHandshakeTimeout *int `yaml:"tls_handshake_timeout"` | ||
ResponseHeaderTimeout *int `yaml:"response_header_timeout"` | ||
} | ||
|
||
func (t *Timeout) SetDefaults() { | ||
if t.ConnectTimeout == nil { | ||
t.ConnectTimeout = new(int) | ||
*t.ConnectTimeout = 5 | ||
} | ||
if t.TLSHandshakeTimeout == nil { | ||
t.TLSHandshakeTimeout = new(int) | ||
*t.TLSHandshakeTimeout = 5 | ||
} | ||
if t.ResponseHeaderTimeout == nil { | ||
t.ResponseHeaderTimeout = new(int) | ||
*t.ResponseHeaderTimeout = 5 | ||
} | ||
} | ||
|
||
// AppSecConfig is a struct that holds the configuration for the AppSec. | ||
type AppSecConfig struct { | ||
Url string `yaml:"url"` | ||
InsecureSkipVerify *bool `yaml:"insecure_skip_verify"` | ||
CAPath string `yaml:"ca_cert_path"` | ||
ParsedUrl *url.URL `yaml:"-"` | ||
Timeout Timeout `yaml:"timeout"` | ||
} | ||
|
||
// AppSec is a struct that holds the configuration for the AppSec. Inherits the API key from the bouncer config. | ||
type AppSec struct { | ||
APIKey string `yaml:"api_key"` | ||
AppSecConfig *AppSecConfig `yaml:"appsec_config"` | ||
Client *http.Client `yaml:"-"` | ||
} | ||
|
||
type AppSecResponse struct { | ||
Response *http.Response | ||
Action string `json:"action"` | ||
HTTPStatus int `json:"http_status"` | ||
} | ||
|
||
// Config() fills the struct with configuration values from a file. It is not | ||
// aware of .yaml.local files so it is recommended to use ConfigReader() instead. | ||
func (w *AppSec) Config(configPath string) error { | ||
reader, err := os.Open(configPath) | ||
if err != nil { | ||
return fmt.Errorf("unable to read config file '%s': %w", configPath, err) | ||
} | ||
|
||
return w.ConfigReader(reader) | ||
} | ||
|
||
func (w *AppSec) ConfigReader(configReader io.Reader) error { | ||
content, err := io.ReadAll(configReader) | ||
if err != nil { | ||
return fmt.Errorf("unable to read configuration: %w", err) | ||
} | ||
|
||
err = yaml.Unmarshal(content, w) | ||
if err != nil { | ||
return fmt.Errorf("unable to unmarshal config file: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (w *AppSec) Init() error { | ||
var err error | ||
|
||
if w.AppSecConfig.Url == "" { | ||
return fmt.Errorf("config does not contain AppSec url") | ||
} | ||
|
||
if w.AppSecConfig.ParsedUrl, err = url.Parse(w.AppSecConfig.Url); err != nil { | ||
return fmt.Errorf("unable to parse AppSec url: %w", err) | ||
} | ||
|
||
if w.AppSecConfig.InsecureSkipVerify == nil { | ||
w.AppSecConfig.InsecureSkipVerify = new(bool) | ||
*w.AppSecConfig.InsecureSkipVerify = false | ||
} | ||
|
||
w.AppSecConfig.Timeout.SetDefaults() | ||
|
||
caCertPool, err := getCertPool(w.AppSecConfig.CAPath, log.StandardLogger()) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
dialContext := (&net.Dialer{ | ||
Timeout: time.Duration(*w.AppSecConfig.Timeout.ConnectTimeout) * time.Second, | ||
}).DialContext | ||
|
||
if w.AppSecConfig.ParsedUrl.Scheme == "unix" || strings.HasPrefix(w.AppSecConfig.ParsedUrl.String(), "/") { | ||
dialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { | ||
return net.Dial("unix", strings.TrimSuffix(w.AppSecConfig.ParsedUrl.Path, "/")) | ||
} | ||
w.AppSecConfig.ParsedUrl.Host = "unix" | ||
w.AppSecConfig.ParsedUrl.Scheme = "http" | ||
} | ||
|
||
w.Client = &http.Client{ | ||
Transport: &http.Transport{ | ||
DialContext: dialContext, | ||
TLSClientConfig: &tls.Config{ | ||
InsecureSkipVerify: *w.AppSecConfig.InsecureSkipVerify, | ||
RootCAs: caCertPool, | ||
}, | ||
TLSHandshakeTimeout: time.Duration(*w.AppSecConfig.Timeout.TLSHandshakeTimeout) * time.Second, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded second is way too long (see my comment above in the |
||
ResponseHeaderTimeout: time.Duration(*w.AppSecConfig.Timeout.ResponseHeaderTimeout) * time.Second, | ||
}, | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// ParseClientReq parses the client request and returns a new request that is ready to be forwarded to the AppSec. | ||
// You can override the IP address with the ipOverride parameter. | ||
// This function should not be used directly, use Forward() instead. | ||
func (w *AppSec) ParseClientReq(ctx context.Context, clientReq *http.Request, ipOverride string) (*http.Request, error) { | ||
var req *http.Request | ||
|
||
if clientReq.Body != nil && clientReq.ContentLength > 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need some kind of override for the body: in some situations, we do not want to read the entire body in memory (for example, a file upload of a few GB). Cleaner solution would be to provide additional configuration with for example body size, URL prefix, source IP and so on (this is an issue we already have in the nginx appsec integration) |
||
bodyBytes, err := io.ReadAll(clientReq.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
clientReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) | ||
req, _ = http.NewRequestWithContext(ctx, http.MethodPost, w.AppSecConfig.ParsedUrl.String(), bytes.NewBuffer(bodyBytes)) | ||
} else { | ||
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, w.AppSecConfig.ParsedUrl.String(), nil) | ||
} | ||
|
||
for key, headers := range clientReq.Header { | ||
for _, value := range headers { | ||
req.Header.Add(key, value) | ||
} | ||
} | ||
|
||
if ipOverride != "" { | ||
req.Header.Set(crowdsecAppsecIPHeader, ipOverride) | ||
} else { | ||
req.Header.Set(crowdsecAppsecIPHeader, clientReq.RemoteAddr) | ||
} | ||
|
||
req.Header.Set(crowdsecAppsecHeader, w.APIKey) | ||
req.Header.Set(crowdsecAppsecVerbHeader, clientReq.Method) | ||
req.Header.Set(crowdsecAppsecHostHeader, clientReq.Host) | ||
req.Header.Set(crowdsecAppsecURIHeader, clientReq.URL.String()) | ||
req.Header.Set(crowdsecAppsecUserAgent, clientReq.Header.Get("User-Agent")) | ||
|
||
return req, nil | ||
} | ||
|
||
// Internal forward function that sends the request to the AppSec and returns the response. | ||
func (w *AppSec) forward(req *http.Request) (*AppSecResponse, error) { | ||
res, err := w.Client.Do(req) | ||
|
||
if err != nil { | ||
return &AppSecResponse{Response: res}, fmt.Errorf("appsecQuery %w", err) | ||
} | ||
defer res.Body.Close() | ||
|
||
wr := &AppSecResponse{Response: res} | ||
|
||
if res.StatusCode == http.StatusInternalServerError { | ||
return wr, fmt.Errorf("appsecQuery: unexpected status code %d", res.StatusCode) | ||
} | ||
|
||
if err := json.NewDecoder(res.Body).Decode(wr); err != nil { | ||
return wr, fmt.Errorf("appsecQuery %w", err) | ||
} | ||
|
||
return wr, nil | ||
} | ||
|
||
// ForwardCTXWithIP forwards the request to the AppSec and returns the response. | ||
// You can override the IP address with the IP parameter. | ||
// You do not need to parse the client request, just pass it as an argument. | ||
// You can pass a context as the first argument. This is useful if you want to cancel the request. | ||
func (w *AppSec) ForwardCTXWithIP(ctx context.Context, clientReq *http.Request, IP string) (*AppSecResponse, error) { | ||
req, err := w.ParseClientReq(ctx, clientReq, IP) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("appsecQuery %w", err) | ||
} | ||
|
||
return w.forward(req) | ||
} | ||
|
||
// ForwardWithIP forwards the request to the AppSec and returns the response. | ||
// You can override the IP address with the IP parameter. | ||
// You do not need to parse the client request, just pass it as an argument. | ||
func (w *AppSec) ForwardWithIP(clientReq *http.Request, IP string) (*AppSecResponse, error) { | ||
req, err := w.ParseClientReq(context.Background(), clientReq, IP) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("appsecQuery %w", err) | ||
} | ||
|
||
return w.forward(req) | ||
} | ||
|
||
// ForwardCTX forwards the request to the AppSec and returns the response. | ||
// You do not need to parse the client request, just pass it as an argument. | ||
// You can pass a context as the first argument. This is useful if you want to cancel the request. | ||
func (w *AppSec) ForwardCTX(ctx context.Context, clientReq *http.Request) (*AppSecResponse, error) { | ||
req, err := w.ParseClientReq(ctx, clientReq, "") | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("appsecQuery %w", err) | ||
} | ||
|
||
return w.forward(req) | ||
} | ||
|
||
// Forward forwards the request to the AppSec and returns the response. | ||
// You do not need to parse the client request, just pass it as an argument. | ||
func (w *AppSec) Forward(clientReq *http.Request) (*AppSecResponse, error) { | ||
req, err := w.ParseClientReq(context.Background(), clientReq, "") | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("appsecQuery %w", err) | ||
} | ||
|
||
return w.forward(req) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename to
AppsecTimeout
asTimeout
might be misunderstood as also applying to the connection to LAPI itself ?