diff --git a/ymt_v3_2/client.go b/ymt_v3_2/client.go new file mode 100644 index 0000000..4d04274 --- /dev/null +++ b/ymt_v3_2/client.go @@ -0,0 +1,188 @@ +package ymt_v3_2 + +import ( + "bytes" + "context" + "crypto/rsa" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type Client struct { + httpClient *http.Client + baseURL string + appID string + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey + aesKey []byte + signType string +} + +type Option func(*Client) + +func WithBaseURL(url string) Option { + return func(c *Client) { + c.baseURL = url + } +} + +func WithTimeout(d time.Duration) Option { + return func(c *Client) { + c.httpClient.Timeout = d + } +} + +func WithHTTPClient(client *http.Client) Option { + return func(c *Client) { + c.httpClient = client + } +} + +func WithAppID(appID string) Option { + return func(c *Client) { + c.appID = appID + } +} + +func WithPrivateKeyPEM(pemData string) Option { + return func(c *Client) { + key, err := parsePrivateKey(pemData) + if err != nil { + panic(fmt.Sprintf("failed to parse private key: %v", err)) + } + c.privateKey = key + } +} + +func WithPublicKeyPEM(pemData string) Option { + return func(c *Client) { + key, err := parsePublicKey(pemData) + if err != nil { + panic(fmt.Sprintf("failed to parse public key: %v", err)) + } + c.publicKey = key + } +} + +func WithAESKey(key string) Option { + return func(c *Client) { + c.aesKey = []byte(key) + } +} + +func WithSignType(signType string) Option { + return func(c *Client) { + c.signType = signType + } +} + +func NewClient(opts ...Option) *Client { + c := &Client{ + httpClient: &http.Client{Timeout: 30 * time.Second}, + baseURL: "https://gateway.dev.cdlsxd.cn", + signType: "RSA", + } + for _, opt := range opts { + opt(c) + } + return c +} + +func (c *Client) Do(ctx context.Context, method, path string, bizReq interface{}, bizResp interface{}) error { + plaintext, err := marshalSortedNoZero(bizReq) + if err != nil { + return fmt.Errorf("marshal request: %w", err) + } + + ciphertext, err := aesEncryptECB(plaintext, c.aesKey) + if err != nil { + return fmt.Errorf("encrypt: %w", err) + } + + timestamp := time.Now().Format("2006-01-02 15:04:05") + + signStr := c.appID + timestamp + ciphertext + sign, err := rsaSign([]byte(signStr), c.privateKey) + if err != nil { + return fmt.Errorf("sign: %w", err) + } + + reqBody := cipherRequest{Ciphertext: ciphertext} + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("marshal request body: %w", err) + } + + url := c.baseURL + path + req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(bodyBytes)) + if err != nil { + return fmt.Errorf("create request: %w", err) + } + req.Header.Set("Appid", c.appID) + req.Header.Set("Timestamp", timestamp) + req.Header.Set("Sign", sign) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("http request: %w", err) + } + defer resp.Body.Close() + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response: %w", err) + } + + var commonResp commonResponse + if err := json.Unmarshal(respBytes, &commonResp); err != nil { + return fmt.Errorf("unmarshal response: %w", err) + } + + if commonResp.Code != 200 { + return &APIError{ + Code: commonResp.Code, + Message: commonResp.Message, + Reason: commonResp.Reason, + } + } + + if len(commonResp.Data) == 0 { + return nil + } + + var cd cipherData + if err := json.Unmarshal(commonResp.Data, &cd); err == nil && cd.Ciphertext != "" { + decrypted, err := aesDecryptECB(cd.Ciphertext, c.aesKey) + if err != nil { + return fmt.Errorf("decrypt response: %w", err) + } + if err := json.Unmarshal(decrypted, bizResp); err != nil { + return fmt.Errorf("unmarshal decrypted response: %w", err) + } + } else { + if err := json.Unmarshal(commonResp.Data, bizResp); err != nil { + return fmt.Errorf("unmarshal plain data: %w", err) + } + } + + return nil +} + +type cipherRequest struct { + Ciphertext string `json:"ciphertext"` +} + +type commonResponse struct { + Code int32 `json:"code"` + Message string `json:"message"` + Reason string `json:"reason,omitempty"` + Data json.RawMessage `json:"data,omitempty"` +} + +type cipherData struct { + Ciphertext string `json:"ciphertext"` +} \ No newline at end of file