// Copyright (C) 2023 Marius Schellenberger package client import ( "bytes" "context" "errors" "fmt" "io" "net" "net/http" "net/http/httputil" "os" "strings" "time" ) type Client struct { endpoint string httpClient *http.Client debugWriter io.Writer //insecure bool } var ( ErrNotFound = errors.New("not found") ) type ClientOption func(*Client) func WithEndpoint(endpoint string) ClientOption { return func(client *Client) { client.endpoint = strings.TrimRight(endpoint, "/") } } func WithDebugWriter(debugWriter io.Writer) ClientOption { return func(client *Client) { client.debugWriter = debugWriter } } func WithHTTPClient(httpClient *http.Client) ClientOption { return func(client *Client) { client.httpClient = httpClient } } //func WithInsecureClient(insecure bool) ClientOption { // return func(client *Client) { // client.insecure = insecure // } //} var ( ErrMissingEndpointEnv = errors.New("missing environment variable KEYCTL_ENDPOINT") ErrMissingEndpoint = errors.New("missing Endpoint in config") ) type Config struct { Endpoint string `json:"endpoint"` //Insecure bool `json:"insecure"` } func ClientOptionsFromConfig(cfg Config) (options []ClientOption, err error) { if cfg.Endpoint == "" { err = ErrMissingEndpoint return } options = []ClientOption{ WithEndpoint(cfg.Endpoint), //WithInsecureClient(cfg.Insecure), } return } func ClientOptionsFromEnv() (options []ClientOption, err error) { endpoint := os.Getenv("KEYCTL_ENDPOINT") if endpoint == "" { err = ErrMissingEndpointEnv return } //insecure, _ := strconv.ParseBool(os.Getenv("HAPROXY_LB_INSECURE")) options = []ClientOption{ WithEndpoint(endpoint), //WithInsecureClient(insecure), } return } func NewClient(options ...ClientOption) *Client { client := &Client{} for _, option := range options { option(client) } if client.httpClient == nil { client.httpClient = &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, //ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, //TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, //TLSClientConfig: &tls.Config{ // InsecureSkipVerify: client.insecure, //}, }, } } return client } func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { url := c.endpoint + path req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } req = req.WithContext(ctx) return req, nil } func (c *Client) Do(r *http.Request, v any) (resp *http.Response, err error) { if c.debugWriter != nil { dumpReq, err := httputil.DumpRequestOut(r, true) if err != nil { return &http.Response{}, err } fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq) } resp, err = c.httpClient.Do(r) if err != nil { return } body, err := io.ReadAll(resp.Body) if err != nil { resp.Body.Close() return } resp.Body.Close() resp.Body = io.NopCloser(bytes.NewReader(body)) if c.debugWriter != nil { dumpResp, err := httputil.DumpResponse(resp, true) if err != nil { return resp, err } fmt.Fprintf(c.debugWriter, "--- Response:\n%s\n\n", dumpResp) } err = decodeResp(resp, v) return } func (c *Client) FollowRedirect(r *http.Request, v any, delay time.Duration) (resp *http.Response, err error) { rr := r for { resp, err = c.Do(rr, v) if !errors.Is(ErrRedirect, err) { return } rdr := err.(Redirect) rr, err = c.NewRequest(r.Context(), "GET", rdr.Location, nil) if err != nil { return } time.Sleep(delay) } }