Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/internal/gzev/sms.go
T
lingniu d6b957dfc8 feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates.

Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
2026-10-26 18:05:59 +08:00

123 lines
4.4 KiB
Go

package gzev
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"strings"
"time"
)
type SMS struct {
AccessKeyID, AccessKeySecret, SecurityToken, SignName, TemplateCode, TemplateParam string
HTTP *http.Client
}
func SMSFromEnv() SMS {
return SMS{AccessKeyID: os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), AccessKeySecret: os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"), SecurityToken: os.Getenv("ALIBABA_CLOUD_SECURITY_TOKEN"), SignName: os.Getenv("GZEV_SMS_SIGN_NAME"), TemplateCode: os.Getenv("GZEV_SMS_TEMPLATE_CODE"), TemplateParam: os.Getenv("GZEV_SMS_TEMPLATE_PARAM"), HTTP: &http.Client{Timeout: 20 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}}
}
func hashHex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }
// ACS3 native HTTP signing follows Alibaba Cloud's published V3 specification.
func signACS3(req *http.Request, payload, accessKey, secret string) {
req.Header.Set("x-acs-content-sha256", hashHex([]byte(payload)))
headers := map[string]string{"host": req.URL.Host}
for key, values := range req.Header {
k := strings.ToLower(key)
if k == "content-type" || strings.HasPrefix(k, "x-acs-") {
headers[k] = strings.TrimSpace(strings.Join(values, ","))
}
}
names := make([]string, 0, len(headers))
for k := range headers {
names = append(names, k)
}
sort.Strings(names)
var canonical strings.Builder
for _, k := range names {
fmt.Fprintf(&canonical, "%s:%s\n", k, headers[k])
}
signed := strings.Join(names, ";")
canonicalRequest := req.Method + "\n/\n" + req.URL.RawQuery + "\n" + canonical.String() + "\n" + signed + "\n" + hashHex([]byte(payload))
stringToSign := "ACS3-HMAC-SHA256\n" + hashHex([]byte(canonicalRequest))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
req.Header.Set("Authorization", "ACS3-HMAC-SHA256 Credential="+accessKey+",SignedHeaders="+signed+",Signature="+hex.EncodeToString(mac.Sum(nil)))
}
func (s SMS) Send(ctx context.Context) error {
if s.AccessKeyID == "" || s.AccessKeySecret == "" || s.SignName == "" || s.TemplateCode == "" {
return errors.New("Aliyun SMS not configured")
}
param := s.TemplateParam
if param == "" {
param = "{}"
}
var params map[string]string
if json.Unmarshal([]byte(param), &params) != nil || params == nil {
return errors.New("SMS TemplateParam must be a JSON string map")
}
// Template must contain these two variables for a user-actionable recovery message.
uploadURL := os.Getenv("GZEV_SESSION_UPLOAD_URL")
if uploadURL != "" {
if uploadURL != "https://vehicle.d.lnoneos.com/gzev/session/" {
return errors.New("unsupported recovery URL")
}
path := os.Getenv("GZEV_SESSION_FILE")
if path == "" {
path = "/var/lib/lingniu-gzev/session.json"
}
code, err := EnsureRecoveryCode(path + ".recovery.json")
if err != nil {
return errors.New("cannot create recovery code")
}
params["upload_url"] = uploadURL
params["recovery_code"] = code
data, err := json.Marshal(params)
if err != nil {
return err
}
param = string(data)
}
form := url.Values{"PhoneNumbers": {"16605225837"}, "SignName": {s.SignName}, "TemplateCode": {s.TemplateCode}, "TemplateParam": {param}}
payload := form.Encode()
req, err := http.NewRequestWithContext(ctx, "POST", "https://dysmsapi.aliyuncs.com/", strings.NewReader(payload))
if err != nil {
return err
}
nonce := make([]byte, 16)
if _, err = rand.Read(nonce); err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("x-acs-action", "SendSms")
req.Header.Set("x-acs-version", "2017-05-25")
req.Header.Set("x-acs-date", time.Now().UTC().Format("2006-01-02T15:04:05Z"))
req.Header.Set("x-acs-signature-nonce", hex.EncodeToString(nonce))
if s.SecurityToken != "" {
req.Header.Set("x-acs-security-token", s.SecurityToken)
}
signACS3(req, payload, s.AccessKeyID, s.AccessKeySecret)
resp, err := s.HTTP.Do(req)
if err != nil {
return errors.New("SMS transport failure; delivery status may be unknown")
}
defer resp.Body.Close()
var result struct {
Code string `json:"Code"`
}
if json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result) != nil || resp.StatusCode != 200 || result.Code != "OK" {
return errors.New("SMS provider did not confirm acceptance")
}
return nil
}