chore: initialize go-sip repository

This commit is contained in:
2026-09-21 08:56:04 +08:00
commit 643b11b21f
309 changed files with 40521 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
package oss
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
agentv1 "git.ipao.vip/rogee/go-sip/gen/agent/v1"
aliyunoss "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
)
const defaultGrantTTL = 15 * time.Minute
// Config is Dispatcher-owned OSS configuration. Access keys never leave the
// Dispatcher; Agents receive only presigned upload grants.
type Config struct {
Endpoint string
Region string
Bucket string
AccessKeyID string
AccessKeySecret string
KeyPrefix string
GrantTTL time.Duration
MaxAssetBytes int64
}
func (c Config) Validate() error {
if strings.TrimSpace(c.Endpoint) == "" || strings.TrimSpace(c.Region) == "" || strings.TrimSpace(c.Bucket) == "" {
return errors.New("OSS endpoint, region and bucket are required")
}
if strings.TrimSpace(c.AccessKeyID) == "" || strings.TrimSpace(c.AccessKeySecret) == "" {
return errors.New("OSS access key ID and secret are required")
}
endpoint := c.Endpoint
if !strings.Contains(endpoint, "://") {
endpoint = "https://" + endpoint
}
parsed, err := url.Parse(endpoint)
if err != nil || parsed.Host == "" || parsed.Scheme != "https" {
return errors.New("OSS endpoint must be a valid HTTPS URL")
}
if c.GrantTTL <= 0 || c.GrantTTL > 7*24*time.Hour {
return errors.New("OSS grant TTL must be between 1 second and 7 days")
}
if c.MaxAssetBytes <= 0 {
return errors.New("OSS max asset bytes must be positive")
}
return nil
}
// Client signs and verifies object operations using Alibaba's official OSS
// SDK. It is intentionally only constructed in the Dispatcher process.
type Client struct {
config Config
client *aliyunoss.Client
}
func NewClient(config Config) (*Client, error) {
if config.GrantTTL == 0 {
config.GrantTTL = defaultGrantTTL
}
if err := config.Validate(); err != nil {
return nil, err
}
endpoint := config.Endpoint
if !strings.Contains(endpoint, "://") {
endpoint = "https://" + endpoint
}
sdkConfig := aliyunoss.LoadDefaultConfig().
WithCredentialsProvider(credentials.NewStaticCredentialsProvider(config.AccessKeyID, config.AccessKeySecret)).
WithRegion(config.Region).
WithEndpoint(endpoint).
WithSignatureVersion(aliyunoss.SignatureVersionV4)
return &Client{config: config, client: aliyunoss.NewClient(sdkConfig)}, nil
}
func (c *Client) Config() Config { return c.config }
func (c *Client) Grant(ctx context.Context, uploadID, objectKey, checksum string, maxBytes int64, now time.Time) (*agentv1.UploadGrant, error) {
if c == nil || c.client == nil {
return nil, errors.New("OSS client is not configured")
}
if uploadID == "" || objectKey == "" || checksum == "" {
return nil, errors.New("upload ID, object key and checksum are required")
}
if maxBytes <= 0 || maxBytes > c.config.MaxAssetBytes {
maxBytes = c.config.MaxAssetBytes
}
if now.IsZero() {
now = time.Now()
}
expiresAt := now.Add(c.config.GrantTTL)
request := &aliyunoss.PutObjectRequest{
Bucket: aliyunoss.Ptr(c.config.Bucket),
Key: aliyunoss.Ptr(objectKey),
Metadata: map[string]string{
"sha256": checksum,
},
}
presigned, err := c.client.Presign(ctx, request, aliyunoss.PresignExpiration(expiresAt))
if err != nil {
return nil, fmt.Errorf("presign OSS PUT: %w", err)
}
headers := make([]*agentv1.Header, 0, len(presigned.SignedHeaders))
for name, value := range presigned.SignedHeaders {
headers = append(headers, &agentv1.Header{Name: name, Value: value})
}
return &agentv1.UploadGrant{
UploadId: uploadID,
TargetUrl: presigned.URL,
Headers: headers,
ExpiresAtUnixMs: presigned.Expiration.UnixMilli(),
ObjectKey: objectKey,
RequiredChecksumSha256: checksum,
MaxBytes: maxBytes,
}, nil
}
func (c *Client) Verify(ctx context.Context, objectKey, checksum string, size int64) error {
if c == nil || c.client == nil {
return errors.New("OSS client is not configured")
}
result, err := c.client.HeadObject(ctx, &aliyunoss.HeadObjectRequest{
Bucket: aliyunoss.Ptr(c.config.Bucket),
Key: aliyunoss.Ptr(objectKey),
})
if err != nil {
return fmt.Errorf("head OSS object: %w", err)
}
if result.ContentLength != size {
return fmt.Errorf("OSS object size mismatch: expected %d got %d", size, result.ContentLength)
}
storedChecksum := ""
for key, value := range result.Metadata {
key = strings.ToLower(strings.TrimSpace(key))
key = strings.TrimPrefix(key, "x-oss-meta-")
if key == "sha256" {
storedChecksum = value
break
}
}
if !strings.EqualFold(strings.TrimSpace(checksum), strings.TrimSpace(storedChecksum)) {
return errors.New("OSS object checksum metadata mismatch")
}
return nil
}
func (c *Client) ObjectID(objectKey string) string {
return "oss://" + c.config.Bucket + "/" + strings.TrimPrefix(objectKey, "/")
}
+75
View File
@@ -0,0 +1,75 @@
package oss_test
import (
"context"
"crypto/sha256"
"encoding/hex"
"os"
"strings"
"testing"
"time"
"git.ipao.vip/rogee/go-sip/internal/agent"
ossclient "git.ipao.vip/rogee/go-sip/internal/oss"
)
func TestAlibabaOSSGrantPutHeadIntegration(t *testing.T) {
if os.Getenv("AGENT_CALL_OSS_INTEGRATION") != "1" {
t.Skip("set AGENT_CALL_OSS_INTEGRATION=1 to use the authorized Alibaba OSS test bucket")
}
config := ossclient.Config{
Endpoint: os.Getenv("DISPATCHER_OSS_ENDPOINT"),
Region: os.Getenv("DISPATCHER_OSS_REGION"),
Bucket: os.Getenv("DISPATCHER_OSS_BUCKET"),
AccessKeyID: os.Getenv("DISPATCHER_OSS_ACCESS_KEY_ID"),
AccessKeySecret: os.Getenv("DISPATCHER_OSS_ACCESS_KEY_SECRET"),
KeyPrefix: "agent-call/integration-tests",
GrantTTL: 15 * time.Minute,
MaxAssetBytes: 1 << 20,
}
client, err := ossclient.NewClient(config)
if err != nil {
t.Fatal(err)
}
payload := []byte(strings.Repeat("agent-call-oss-integration\n", 1024))
digest := sha256.Sum256(payload)
checksum := hex.EncodeToString(digest[:])
objectKey := "agent-call/integration-tests/" + checksum + ".txt"
grant, err := client.Grant(context.Background(), "integration-"+checksum[:16], objectKey, checksum, int64(len(payload)), time.Now())
if err != nil {
t.Fatal(err)
}
file, err := os.CreateTemp(t.TempDir(), "oss-upload-*.txt")
if err != nil {
t.Fatal(err)
}
if _, err := file.Write(payload); err != nil {
_ = file.Close()
t.Fatal(err)
}
if err := file.Close(); err != nil {
t.Fatal(err)
}
uploader := agent.UploadClient{Now: time.Now, AllowedHosts: map[string]struct{}{}}
result, err := uploader.UploadFile(context.Background(), grant, file.Name())
if err != nil {
t.Fatalf("upload failed without exposing the presigned URL: %s", redactError(err, config.AccessKeyID))
}
if result.SizeBytes != int64(len(payload)) || !strings.EqualFold(result.SHA256, checksum) {
t.Fatalf("upload result mismatch: %+v", result)
}
if err := client.Verify(context.Background(), objectKey, checksum, int64(len(payload))); err != nil {
t.Fatal(err)
}
}
func redactError(err error, accessKeyID string) string {
if err == nil {
return ""
}
message := err.Error()
if accessKeyID != "" {
message = strings.ReplaceAll(message, accessKeyID, "<access-key-id>")
}
return message
}