package hfb import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) const uploadPath = "/api/open/listing-uploads" type Client struct { Server string Secret string Timeout time.Duration HTTPClient *http.Client } func (c Client) Upload(ctx context.Context, req ExternalUploadRequest) (string, error) { body, err := json.Marshal(req) if err != nil { return "", fmt.Errorf("生成上传 JSON 失败:%w", err) } endpoint, err := normalizeEndpoint(c.Server) if err != nil { return "", err } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { return "", fmt.Errorf("创建上传请求失败:%w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("User-Agent", "updata_hfb/1.0") if strings.TrimSpace(c.Secret) != "" { timestamp := strconvFormatUnix(time.Now()) httpReq.Header.Set("X-HFB-Timestamp", timestamp) httpReq.Header.Set("X-HFB-Signature", signBody(c.Secret, timestamp, body)) } httpClient := c.HTTPClient if httpClient == nil { timeout := c.Timeout if timeout <= 0 { timeout = 15 * time.Second } httpClient = &http.Client{Timeout: timeout} } resp, err := httpClient.Do(httpReq) if err != nil { return "", fmt.Errorf("请求服务器失败:%w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return "", fmt.Errorf("读取服务器响应失败:%w", err) } text := strings.TrimSpace(string(respBody)) if resp.StatusCode < 200 || resp.StatusCode >= 300 { if text == "" { text = resp.Status } return "", fmt.Errorf("服务器返回失败状态 %s:%s", resp.Status, text) } if text == "" { return resp.Status, nil } return text, nil } func normalizeEndpoint(raw string) (string, error) { raw = strings.TrimSpace(raw) if raw == "" { return "", fmt.Errorf("服务器地址不能为空") } parsed, err := url.Parse(raw) if err != nil || parsed.Scheme == "" || parsed.Host == "" { return "", fmt.Errorf("服务器地址不正确:%s", raw) } if strings.Contains(parsed.Path, uploadPath) { return parsed.String(), nil } basePath := strings.TrimRight(parsed.Path, "/") parsed.Path = basePath + uploadPath return parsed.String(), nil } func signBody(secret, timestamp string, body []byte) string { mac := hmac.New(sha256.New, []byte(strings.TrimSpace(secret))) mac.Write([]byte(timestamp)) mac.Write([]byte(".")) mac.Write(body) return hex.EncodeToString(mac.Sum(nil)) } func strconvFormatUnix(t time.Time) string { return fmt.Sprintf("%d", t.Unix()) }