feat(payment): 虎皮椒聚合支付 - 官方插件与跨平台可执行文件
新增 xunhupay 官方包,支付 RPC 与宿主只保留订单和验签边界;官方支付包同时带上各系统 provider,避免把 Linux ELF 当成本机入口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e5a5f0ccc8
commit
a23df0cb32
63 files changed
+2070
-128
No files matched your search
+197
-26
@@ -10,6 +10,8 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"debug/elf"
|
||||
"debug/macho"
|
||||
"debug/pe"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -25,6 +27,8 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,6 +39,7 @@ const (
|
||||
|
||||
type RPCProvider struct {
|
||||
descriptor Descriptor
|
||||
entry string
|
||||
command string
|
||||
dir string
|
||||
}
|
||||
@@ -49,7 +54,11 @@ func NewRPCProvider(descriptor Descriptor, packageDir, entry string) (*RPCProvid
|
||||
if strings.TrimSpace(packageDir) == "" {
|
||||
return nil, errors.New("payment plugin package directory is empty")
|
||||
}
|
||||
return &RPCProvider{descriptor: descriptor, command: filepath.Join(packageDir, filepath.FromSlash(entry)), dir: packageDir}, nil
|
||||
packageDir, err := filepath.Abs(packageDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("payment plugin package directory is invalid: %w", err)
|
||||
}
|
||||
return &RPCProvider{descriptor: descriptor, entry: entry, dir: packageDir}, nil
|
||||
}
|
||||
|
||||
func (p *RPCProvider) Descriptor() Descriptor { return p.descriptor }
|
||||
@@ -73,10 +82,14 @@ type rpcResponse struct {
|
||||
}
|
||||
|
||||
func (p *RPCProvider) call(ctx context.Context, request rpcRequest, output any) error {
|
||||
if p == nil || strings.TrimSpace(p.command) == "" {
|
||||
if p == nil {
|
||||
return errors.New("payment plugin runtime is unavailable")
|
||||
}
|
||||
info, err := os.Stat(p.command)
|
||||
command, err := p.executable()
|
||||
if err != nil {
|
||||
return p.startError(err)
|
||||
}
|
||||
info, err := os.Stat(command)
|
||||
if err != nil {
|
||||
return p.startError(err)
|
||||
}
|
||||
@@ -86,7 +99,7 @@ func (p *RPCProvider) call(ctx context.Context, request rpcRequest, output any)
|
||||
if runtime.GOOS != "windows" && info.Mode().Perm()&0o111 == 0 {
|
||||
return p.startError(fmt.Errorf("payment plugin executable has no execute bit: %w", syscall.EACCES))
|
||||
}
|
||||
if err := validatePaymentExecutablePlatform(p.command); err != nil {
|
||||
if err := validatePaymentExecutablePlatform(command); err != nil {
|
||||
return p.startError(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, pluginRPCTimeout)
|
||||
@@ -95,9 +108,9 @@ func (p *RPCProvider) call(ctx context.Context, request rpcRequest, output any)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, p.command)
|
||||
cmd := exec.CommandContext(ctx, command)
|
||||
cmd.Dir = p.dir
|
||||
cmd.Env = []string{"PATH=/usr/bin:/bin", "HOME=/nonexistent"}
|
||||
cmd.Env = paymentPluginCommandEnv()
|
||||
cmd.Stdin = strings.NewReader(string(payload) + "\n")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
@@ -148,21 +161,96 @@ func (p *RPCProvider) call(ctx context.Context, request rpcRequest, output any)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *RPCProvider) executable() (string, error) {
|
||||
if p == nil {
|
||||
return "", errors.New("payment plugin runtime is unavailable")
|
||||
}
|
||||
if strings.TrimSpace(p.command) != "" {
|
||||
return p.command, nil
|
||||
}
|
||||
command, err := ResolveRPCBackendPath(p.dir, p.entry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
command, err = filepath.Abs(command)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p.command = command
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (p *RPCProvider) startError(cause error) error {
|
||||
providerErr := classifyPaymentPluginStartError(cause)
|
||||
command := strings.TrimSpace(p.command)
|
||||
if command == "" {
|
||||
command, _ = ResolveRPCBackendPath(p.dir, p.entry)
|
||||
}
|
||||
log.Printf(
|
||||
"payment plugin executable failed: provider=%q plugin=%q host=%s/%s command=%q code=%s error=%v",
|
||||
p.descriptor.ID,
|
||||
p.descriptor.PluginID,
|
||||
runtime.GOOS,
|
||||
runtime.GOARCH,
|
||||
p.command,
|
||||
command,
|
||||
providerErr.Code,
|
||||
cause,
|
||||
)
|
||||
return providerErr
|
||||
}
|
||||
|
||||
func paymentPluginCommandEnv() []string {
|
||||
if runtime.GOOS == "windows" {
|
||||
systemRoot := strings.TrimSpace(os.Getenv("SystemRoot"))
|
||||
if systemRoot == "" {
|
||||
systemRoot = strings.TrimSpace(os.Getenv("SYSTEMROOT"))
|
||||
}
|
||||
env := []string{"HOME=" + filepath.Join("C:\\", "nonexistent")}
|
||||
if systemRoot != "" {
|
||||
env = append(env, "SystemRoot="+systemRoot, "SYSTEMROOT="+systemRoot)
|
||||
}
|
||||
return env
|
||||
}
|
||||
return []string{"PATH=/usr/bin:/bin", "HOME=/nonexistent"}
|
||||
}
|
||||
|
||||
func ResolveRPCBackendPath(packageDir, entry string) (string, error) {
|
||||
if strings.TrimSpace(packageDir) == "" {
|
||||
return "", fmt.Errorf("payment plugin executable for %s/%s is missing: %w", runtime.GOOS, runtime.GOARCH, os.ErrNotExist)
|
||||
}
|
||||
var missingErr, formatErr error
|
||||
for _, candidate := range protocol.PaymentRPCBackendCandidates(entry, runtime.GOOS, runtime.GOARCH) {
|
||||
target := filepath.Join(packageDir, filepath.FromSlash(candidate))
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
if missingErr == nil {
|
||||
missingErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if info.IsDir() {
|
||||
if missingErr == nil {
|
||||
missingErr = os.ErrNotExist
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := validatePaymentExecutablePlatform(target); err != nil {
|
||||
if formatErr == nil {
|
||||
formatErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
if formatErr != nil {
|
||||
return "", fmt.Errorf("payment plugin executable for %s/%s is incompatible: %w", runtime.GOOS, runtime.GOARCH, formatErr)
|
||||
}
|
||||
if missingErr == nil {
|
||||
missingErr = os.ErrNotExist
|
||||
}
|
||||
return "", fmt.Errorf("payment plugin executable for %s/%s is missing: %w", runtime.GOOS, runtime.GOARCH, missingErr)
|
||||
}
|
||||
|
||||
func classifyPaymentPluginStartError(cause error) *ProviderError {
|
||||
switch {
|
||||
case errors.Is(cause, os.ErrNotExist), errors.Is(cause, syscall.ENOENT), errors.Is(cause, exec.ErrNotFound):
|
||||
@@ -177,9 +265,6 @@ func classifyPaymentPluginStartError(cause error) *ProviderError {
|
||||
}
|
||||
|
||||
func validatePaymentExecutablePlatform(command string) error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(command)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -190,26 +275,90 @@ func validatePaymentExecutablePlatform(command string) error {
|
||||
if _, err := io.ReadFull(file, magic[:]); err != nil {
|
||||
return fmt.Errorf("payment plugin executable header is incomplete: %w", syscall.ENOEXEC)
|
||||
}
|
||||
if bytes.Equal(magic[:2], []byte{'M', 'Z'}) {
|
||||
return fmt.Errorf("payment plugin executable is Windows PE on linux/%s: %w", runtime.GOARCH, syscall.ENOEXEC)
|
||||
}
|
||||
if isMachOMagic(magic) {
|
||||
return fmt.Errorf("payment plugin executable is Mach-O on linux/%s: %w", runtime.GOARCH, syscall.ENOEXEC)
|
||||
}
|
||||
if !bytes.Equal(magic[:], []byte{0x7f, 'E', 'L', 'F'}) {
|
||||
// Scripts and other kernel-supported executable formats are left to
|
||||
// cmd.Start. Unknown binary data will be classified from ENOEXEC there.
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if bytes.Equal(magic[:2], []byte{'M', 'Z'}) {
|
||||
return fmt.Errorf("payment plugin executable is Windows PE on linux/%s: %w", runtime.GOARCH, syscall.ENOEXEC)
|
||||
}
|
||||
if isMachOMagic(magic) {
|
||||
return fmt.Errorf("payment plugin executable is Mach-O on linux/%s: %w", runtime.GOARCH, syscall.ENOEXEC)
|
||||
}
|
||||
if !bytes.Equal(magic[:], []byte{0x7f, 'E', 'L', 'F'}) {
|
||||
return nil
|
||||
}
|
||||
binary, err := elf.NewFile(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("payment plugin ELF header is invalid: %w", syscall.ENOEXEC)
|
||||
}
|
||||
defer binary.Close()
|
||||
expected, ok := expectedPaymentELFMachine(runtime.GOARCH)
|
||||
if binary.Class != elf.ELFCLASS64 || (ok && binary.Machine != expected) {
|
||||
return fmt.Errorf("payment plugin ELF architecture mismatch: host=linux/%s class=%s machine=%s: %w", runtime.GOARCH, binary.Class, binary.Machine, syscall.ENOEXEC)
|
||||
}
|
||||
return nil
|
||||
case "darwin":
|
||||
if bytes.Equal(magic[:], []byte{0x7f, 'E', 'L', 'F'}) {
|
||||
return fmt.Errorf("payment plugin executable is ELF on darwin/%s: %w", runtime.GOARCH, syscall.ENOEXEC)
|
||||
}
|
||||
if bytes.Equal(magic[:2], []byte{'M', 'Z'}) {
|
||||
return fmt.Errorf("payment plugin executable is Windows PE on darwin/%s: %w", runtime.GOARCH, syscall.ENOEXEC)
|
||||
}
|
||||
if !isMachOMagic(magic) {
|
||||
return nil
|
||||
}
|
||||
return validateMachOArchitecture(command)
|
||||
case "windows":
|
||||
if bytes.Equal(magic[:], []byte{0x7f, 'E', 'L', 'F'}) {
|
||||
return fmt.Errorf("payment plugin executable is ELF on windows/%s: %w", runtime.GOARCH, syscall.ENOEXEC)
|
||||
}
|
||||
if isMachOMagic(magic) {
|
||||
return fmt.Errorf("payment plugin executable is Mach-O on windows/%s: %w", runtime.GOARCH, syscall.ENOEXEC)
|
||||
}
|
||||
if !bytes.Equal(magic[:2], []byte{'M', 'Z'}) {
|
||||
return nil
|
||||
}
|
||||
return validateWindowsPEArchitecture(command)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
binary, err := elf.NewFile(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("payment plugin ELF header is invalid: %w", syscall.ENOEXEC)
|
||||
func validateMachOArchitecture(command string) error {
|
||||
expected, ok := expectedMachOCPU(runtime.GOARCH)
|
||||
file, err := macho.Open(command)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
if ok && file.Cpu != expected {
|
||||
return fmt.Errorf("payment plugin Mach-O architecture mismatch: host=darwin/%s cpu=%s: %w", runtime.GOARCH, file.Cpu, syscall.ENOEXEC)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer binary.Close()
|
||||
expected, ok := expectedPaymentELFMachine(runtime.GOARCH)
|
||||
if binary.Class != elf.ELFCLASS64 || (ok && binary.Machine != expected) {
|
||||
return fmt.Errorf("payment plugin ELF architecture mismatch: host=linux/%s class=%s machine=%s: %w", runtime.GOARCH, binary.Class, binary.Machine, syscall.ENOEXEC)
|
||||
fat, fatErr := macho.OpenFat(command)
|
||||
if fatErr != nil {
|
||||
return fmt.Errorf("payment plugin Mach-O header is invalid: %w", syscall.ENOEXEC)
|
||||
}
|
||||
defer fat.Close()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, arch := range fat.Arches {
|
||||
if arch.Cpu == expected {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("payment plugin Mach-O architecture mismatch: host=darwin/%s: %w", runtime.GOARCH, syscall.ENOEXEC)
|
||||
}
|
||||
|
||||
func validateWindowsPEArchitecture(command string) error {
|
||||
file, err := pe.Open(command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("payment plugin PE header is invalid: %w", syscall.ENOEXEC)
|
||||
}
|
||||
defer file.Close()
|
||||
expected, ok := expectedPEMachine(runtime.GOARCH)
|
||||
if ok && file.Machine != expected {
|
||||
return fmt.Errorf("payment plugin PE architecture mismatch: host=windows/%s machine=%d: %w", runtime.GOARCH, file.Machine, syscall.ENOEXEC)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -232,6 +381,28 @@ func expectedPaymentELFMachine(arch string) (elf.Machine, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func expectedMachOCPU(arch string) (macho.Cpu, bool) {
|
||||
switch arch {
|
||||
case "amd64":
|
||||
return macho.CpuAmd64, true
|
||||
case "arm64":
|
||||
return macho.CpuArm64, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func expectedPEMachine(arch string) (uint16, bool) {
|
||||
switch arch {
|
||||
case "amd64":
|
||||
return pe.IMAGE_FILE_MACHINE_AMD64, true
|
||||
case "arm64":
|
||||
return pe.IMAGE_FILE_MACHINE_ARM64, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *RPCProvider) ValidateConfig(config Config) error {
|
||||
return p.call(context.Background(), rpcRequest{Version: pluginRPCVersion, Operation: "validate_config", Config: config}, nil)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user