package decortsdk import ( "bytes" "context" "crypto/tls" "fmt" "io" "mime/multipart" "net/http" "reflect" "strconv" "strings" "sync" "time" "github.com/google/go-querystring/query" "repository.basistech.ru/BASIS/decort-golang-sdk/config" "repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker" ) // DecortClient is HTTP-client for platform type DecortClient struct { decortURL string client *http.Client cfg config.Config expiryTime time.Time mutex *sync.Mutex } // Сlient builder func New(cfg config.Config) *DecortClient { if cfg.Retries == 0 { cfg.Retries = 5 } var expiryTime time.Time if cfg.Token != "" { expiryTime = time.Now().AddDate(0, 0, 1) } return &DecortClient{ decortURL: cfg.DecortURL, client: &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ //nolint:gosec InsecureSkipVerify: cfg.SSLSkipVerify, }, }, }, cfg: cfg, expiryTime: expiryTime, mutex: &sync.Mutex{}, } } // CloudAPI builder func (dc *DecortClient) CloudAPI() *cloudapi.CloudAPI { return cloudapi.New(dc) } // CloudBroker builder func (dc *DecortClient) CloudBroker() *cloudbroker.CloudBroker { return cloudbroker.New(dc) } // DecortApiCall method for sending requests to the platform func (dc *DecortClient) DecortApiCall(ctx context.Context, method, url string, params interface{}) ([]byte, error) { values, err := query.Values(params) if err != nil { return nil, err } body := bytes.NewBufferString(values.Encode()) req, err := http.NewRequestWithContext(ctx, method, dc.decortURL+constants.Restmachine+url, body) if err != nil { return nil, err } // get token if err = dc.getToken(ctx); err != nil { return nil, err } // perform request respBytes, err := dc.do(req, "") if err != nil { return nil, err } return respBytes, err } // DecortApiCallMP method for sending requests to the platform func (dc *DecortClient) DecortApiCallMP(ctx context.Context, method, url string, params interface{}) ([]byte, error) { body, ctype, err := multiPartReq(params) if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, method, dc.decortURL+constants.Restmachine+url, body) if err != nil { return nil, err } // get token if err = dc.getToken(ctx); err != nil { return nil, err } // perform request respBytes, err := dc.do(req, ctype) if err != nil { return nil, err } return respBytes, err } func (dc *DecortClient) getToken(ctx context.Context) error { dc.mutex.Lock() defer dc.mutex.Unlock() // new token is not needed if dc.cfg.Token != "" && !time.Now().After(dc.expiryTime) { return nil } // set up request headers and body body := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s&response_type=id_token", dc.cfg.AppID, dc.cfg.AppSecret) bodyReader := strings.NewReader(body) dc.cfg.SSOURL = strings.TrimSuffix(dc.cfg.SSOURL, "/") req, _ := http.NewRequestWithContext(ctx, "POST", dc.cfg.SSOURL+"/v1/oauth/access_token", bodyReader) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") // request token resp, err := dc.client.Do(req) if err != nil || resp == nil { return fmt.Errorf("cannot get token: %w", err) } defer resp.Body.Close() var tokenBytes []byte tokenBytes, err = io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("cannot get token: %w", err) } if resp.StatusCode != 200 { return fmt.Errorf("cannot get token: %s", tokenBytes) } // save token in config token := string(tokenBytes) dc.cfg.Token = token dc.expiryTime = time.Now().AddDate(0, 0, 1) return nil } // do method performs request and returns response as an array of bytes and nil error in case of response status code 200. // In any other cases do returns nil response and error. // Retries are implemented in case of connection reset errors. func (dc *DecortClient) do(req *http.Request, ctype string) ([]byte, error) { // set up request headers and body req.Header.Add("Content-Type", "application/x-www-form-urlencoded") if ctype != "" { req.Header.Set("Content-Type", ctype) } req.Header.Add("Authorization", "bearer "+dc.cfg.Token) req.Header.Set("Accept", "application/json") buf, err := io.ReadAll(req.Body) if err != nil { return nil, err } req.Body.Close() req.Body = io.NopCloser(bytes.NewBuffer(buf)) resp, err := dc.client.Do(req) if resp != nil { defer resp.Body.Close() } // retries logic GOES HERE // get http response //var resp *http.Response //for i := uint64(0); i < dc.cfg.Retries; i++ { // req := req.Clone(req.Context()) // req.Body = io.NopCloser(bytes.NewBuffer(buf)) // // if i > 0 { // time.Sleep(5 * time.Second) // no time sleep for the first request // } // // resp, err = dc.client.Do(req) // // // stop retries on success and close response body // if resp != nil { // defer resp.Body.Close() // } // if err == nil { // break // } // // // retries in case of connection errors with time sleep // if isConnectionError(err) { // continue // } // // // return error in case of non-connection error // return nil, err //} // handle http request errors if err != nil { return nil, err } if resp == nil { return nil, fmt.Errorf("got empty response without error") } // handle successful request respBytes, _ := io.ReadAll(resp.Body) if resp.StatusCode == 200 { return respBytes, nil } // handle errors with status code other than 200 err = fmt.Errorf("%s", respBytes) return nil, fmt.Errorf("could not execute request: %w", err) } // isConnectionError checks if given error falls within specific and associated connection errors //func isConnectionError(err error) bool { // if strings.Contains(err.Error(), "connection reset by peer") { // return true // } // if errors.Is(err, io.EOF) { // return true // } // // return false //} // multiPartReq writes the request structure to the request body, and also returns string of the content-type func multiPartReq(params interface{}) (*bytes.Buffer, string, error) { reqBody := &bytes.Buffer{} writer := multipart.NewWriter(reqBody) values := reflect.ValueOf(params) types := values.Type() defer writer.Close() for i := 0; i < values.NumField(); i++ { if !values.Field(i).IsValid() { continue } if values.Field(i).IsZero() { continue } if file, ok := constants.FileName[types.Field(i).Name]; ok { part, err := writer.CreateFormFile(trimString(types.Field(i)), file) if err != nil { return &bytes.Buffer{}, "", err } _, err = io.Copy(part, strings.NewReader(valueToString(values.Field(i).Interface()))) if err != nil { return &bytes.Buffer{}, "", err } continue } if values.Field(i).Type().Kind() == reflect.Slice { switch slice := values.Field(i).Interface().(type) { case []string: for _, val := range slice { err := writer.WriteField(trimString(types.Field(i)), val) if err != nil { return &bytes.Buffer{}, "", err } } case []uint: for _, val := range slice { err := writer.WriteField(trimString(types.Field(i)), strconv.FormatUint(uint64(val), 10)) if err != nil { return &bytes.Buffer{}, "", err } } case []uint64: for _, val := range slice { err := writer.WriteField(trimString(types.Field(i)), strconv.FormatUint(val, 10)) if err != nil { return &bytes.Buffer{}, "", err } } default: return &bytes.Buffer{}, "", fmt.Errorf("unsupported slice type:%T", slice) } continue } err := writer.WriteField(trimString(types.Field(i)), valueToString(values.Field(i).Interface())) if err != nil { return &bytes.Buffer{}, "", err } } ct := writer.FormDataContentType() return reqBody, ct, nil } func valueToString(a any) string { switch str := a.(type) { case string: return str case uint: return strconv.FormatUint(uint64(str), 10) case uint64: return strconv.FormatUint(str, 10) case bool: return strconv.FormatBool(str) default: return "" } } func trimString(el reflect.StructField) string { return strings.TrimSuffix(el.Tag.Get("url"), ",omitempty") }