在ServerAPIConfig中添加BundleID字段,用于苹果服务器API验证 当BundleID未配置时,尝试从站点自定义数据中获取 删除过时的测试文件
132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
package apple
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
type ServerAPIConfig struct {
|
|
KeyID string
|
|
IssuerID string
|
|
PrivateKey string
|
|
Sandbox bool
|
|
BundleID string
|
|
}
|
|
|
|
func buildAPIToken(cfg ServerAPIConfig) (string, error) {
|
|
header := map[string]interface{}{
|
|
"alg": "ES256",
|
|
"kid": cfg.KeyID,
|
|
"typ": "JWT",
|
|
}
|
|
now := time.Now().Unix()
|
|
payload := map[string]interface{}{
|
|
"iss": cfg.IssuerID,
|
|
"iat": now,
|
|
"exp": now + 1800,
|
|
"aud": "appstoreconnect-v1",
|
|
}
|
|
if cfg.BundleID != "" {
|
|
payload["bid"] = cfg.BundleID
|
|
}
|
|
hb, _ := json.Marshal(header)
|
|
pb, _ := json.Marshal(payload)
|
|
enc := func(b []byte) string {
|
|
return base64.RawURLEncoding.EncodeToString(b)
|
|
}
|
|
unsigned := fmt.Sprintf("%s.%s", enc(hb), enc(pb))
|
|
block, _ := pem.Decode([]byte(cfg.PrivateKey))
|
|
if block == nil {
|
|
return "", fmt.Errorf("invalid private key")
|
|
}
|
|
keyAny, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
priv, ok := keyAny.(*ecdsa.PrivateKey)
|
|
if !ok {
|
|
return "", fmt.Errorf("private key is not ECDSA")
|
|
}
|
|
// Correctly calculate R and S for ES256 (P-256 + SHA-256)
|
|
digest := sha256Sum([]byte(unsigned))
|
|
r, s, err := ecdsa.Sign(rand.Reader, priv, digest)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Concatenate R and S (each 32 bytes for P-256)
|
|
curveBits := priv.Curve.Params().BitSize
|
|
keyBytes := curveBits / 8
|
|
if curveBits%8 > 0 {
|
|
keyBytes += 1
|
|
}
|
|
rBytes := r.Bytes()
|
|
rBytesPadded := make([]byte, keyBytes)
|
|
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
|
|
|
|
sBytes := s.Bytes()
|
|
sBytesPadded := make([]byte, keyBytes)
|
|
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
|
|
|
|
sig := append(rBytesPadded, sBytesPadded...)
|
|
return unsigned + "." + base64.RawURLEncoding.EncodeToString(sig), nil
|
|
}
|
|
|
|
func sha256Sum(b []byte) []byte {
|
|
h := sha256.New()
|
|
h.Write(b)
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
func GetTransactionInfo(cfg ServerAPIConfig, transactionId string) (string, error) {
|
|
token, err := buildAPIToken(cfg)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
try := func(host string) (string, int, string, error) {
|
|
url := fmt.Sprintf("%s/inApps/v1/transactions/%s", host, transactionId)
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", 0, "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
buf := new(bytes.Buffer)
|
|
_, _ = buf.ReadFrom(resp.Body)
|
|
if resp.StatusCode != 200 {
|
|
return "", resp.StatusCode, buf.String(), fmt.Errorf("apple api error: %d", resp.StatusCode)
|
|
}
|
|
var body struct {
|
|
SignedTransactionInfo string `json:"signedTransactionInfo"`
|
|
}
|
|
if err := json.Unmarshal(buf.Bytes(), &body); err != nil {
|
|
return "", resp.StatusCode, buf.String(), err
|
|
}
|
|
return body.SignedTransactionInfo, resp.StatusCode, buf.String(), nil
|
|
}
|
|
primary := "https://api.storekit.itunes.apple.com"
|
|
secondary := "https://api.storekit-sandbox.itunes.apple.com"
|
|
if cfg.Sandbox {
|
|
primary, secondary = secondary, primary
|
|
}
|
|
jws, code, body, err := try(primary)
|
|
if err == nil && jws != "" {
|
|
return jws, nil
|
|
}
|
|
// Fallback to the other environment if primary failed (common when env mismatches)
|
|
jws2, code2, body2, err2 := try(secondary)
|
|
if err2 == nil && jws2 != "" {
|
|
return jws2, nil
|
|
}
|
|
return "", fmt.Errorf("apple api error, primary[%d:%s], secondary[%d:%s]", code, body, code2, body2)
|
|
}
|