107 lines
2.7 KiB
Go
107 lines
2.7 KiB
Go
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())
|
||
}
|