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.
123 lines
4.4 KiB
Go
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), ¶ms) != 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
|
|
}
|