Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/cmd/oneos-local-fixture/main.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

311 lines
12 KiB
Go

// Command oneos-local-fixture is a disposable loopback-only OneOS simulator.
// It is NOT a OneOS implementation or an upstream deployment artifact.
package main
import (
"crypto/aes"
"crypto/rand"
"crypto/rsa"
"crypto/subtle"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"html/template"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
)
const clientID = "vehicle-platform-local"
const tenantID = "local-test"
const password = "LocalOnly123!"
type user struct {
ID, Username, Name, VIN string
Disabled, NoAccess bool
}
type session struct {
User string
ExpiresAt time.Time
}
type ticket struct {
Token string
ExpiresAt time.Time
}
type fixture struct {
mu sync.Mutex
key *rsa.PrivateKey
serviceToken, platformOrigin string
users map[string]*user
sessions map[string]session
tickets map[string]ticket
}
func randomToken() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return base64.RawURLEncoding.EncodeToString(b)
}
func response(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{"code": status, "data": data})
}
func loopbackOrigin(value string) bool {
u, e := url.Parse(value)
if e != nil {
return false
}
ip := net.ParseIP(u.Hostname())
return u.Scheme == "http" && ip != nil && ip.IsLoopback() && u.User == nil && u.RawQuery == "" && u.Fragment == "" && u.Path == ""
}
func main() {
listen := flag.String("listen", "127.0.0.1:20410", "literal loopback listener")
origin := flag.String("platform-origin", "http://127.0.0.1:20400", "local platform origin")
configPath := flag.String("config-file", "", "required private JSON output for local launcher")
flag.Parse()
host, _, e := net.SplitHostPort(*listen)
if e != nil || net.ParseIP(host) == nil || !net.ParseIP(host).IsLoopback() || !loopbackOrigin(*origin) || *configPath == "" {
log.Fatal("literal loopback endpoints and config-file required")
}
key, e := rsa.GenerateKey(rand.Reader, 2048)
if e != nil {
log.Fatal(e)
}
f := &fixture{key: key, serviceToken: randomToken(), platformOrigin: *origin, users: map[string]*user{"alpha": {ID: "101", Username: "alpha", Name: "本地甲组用户", VIN: "LB9A32A24R0LS1426"}, "beta": {ID: "102", Username: "beta", Name: "本地乙组用户", VIN: "LNXNEGRR7SR318212"}, "noaccess": {ID: "103", Username: "noaccess", Name: "本地未授权用户", NoAccess: true}}, sessions: map[string]session{}, tickets: map[string]ticket{}}
public, e := x509.MarshalPKIXPublicKey(&key.PublicKey)
if e != nil {
log.Fatal(e)
}
data, _ := json.Marshal(map[string]string{"baseURL": "http://" + *listen, "publicOrigin": *origin, "issuer": "oneos-local-fixture", "tenantId": tenantID, "clientId": clientID, "serviceToken": f.serviceToken, "publicKey": string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: public}))})
file, e := os.OpenFile(*configPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if e != nil {
log.Fatal("cannot create private local config")
}
_, e = file.Write(data)
file.Close()
if e != nil {
log.Fatal(e)
}
server := &http.Server{Addr: *listen, Handler: f, ReadHeaderTimeout: 3 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second}
log.Printf("Local fixture only: http://%s (accounts alpha/beta/noaccess; no external clients)", *listen)
log.Fatal(server.ListenAndServe())
}
func (f *fixture) issue(name string) (string, session) {
s := session{User: name, ExpiresAt: time.Now().Add(20 * time.Minute)}
token := randomToken()
f.sessions[token] = s
return token, s
}
func (f *fixture) current(r *http.Request) (string, *user, session, bool) {
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
s, ok := f.sessions[token]
u := f.users[s.User]
return token, u, s, ok && u != nil && !u.Disabled && s.ExpiresAt.After(time.Now()) && r.Header.Get("ClientID") == clientID
}
func (f *fixture) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Referrer-Policy", "no-referrer")
if r.URL.Path == "/health" {
response(w, 200, map[string]bool{"localOnly": true})
return
}
if r.URL.Path == "/auth/code" && r.Method == "GET" {
response(w, 200, map[string]any{"captchaEnabled": false, "uuid": "", "img": ""})
return
}
if r.URL.Path == "/auth/login" && r.Method == "POST" {
input, err := f.decryptLogin(r)
if err != nil || input["clientId"] != clientID || input["tenantId"] != tenantID || input["grantType"] != "password" || input["password"] != password || f.users[input["username"]] == nil || f.users[input["username"]].Disabled {
response(w, 401, nil)
return
}
token, s := f.issue(input["username"])
response(w, 200, map[string]any{"access_token": token, "client_id": clientID, "expire_in": int(time.Until(s.ExpiresAt).Seconds())})
return
}
if r.URL.Path == "/auth/inner/v1/sso/vehicle-platform/token-exchange" && r.Method == "POST" {
if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte("Bearer "+f.serviceToken)) != 1 {
response(w, 401, nil)
return
}
var input struct {
Code string `json:"code"`
}
if json.NewDecoder(io.LimitReader(r.Body, 2048)).Decode(&input) != nil {
response(w, 400, nil)
return
}
t, ok := f.tickets[input.Code]
delete(f.tickets, input.Code)
s, live := f.sessions[t.Token]
u := f.users[s.User]
if !ok || !live || !t.ExpiresAt.After(time.Now()) || !s.ExpiresAt.After(time.Now()) || u == nil || u.Disabled {
response(w, 401, nil)
return
}
response(w, 200, map[string]any{"accessToken": t.Token, "tokenType": "Bearer", "clientId": clientID, "expiresAt": s.ExpiresAt})
return
}
if r.URL.Path == "/system/user/getInfo" || r.URL.Path == "/asset/vehicle-platform/scope" || r.URL.Path == "/auth/logout" {
token, u, _, ok := f.current(r)
if !ok {
response(w, 401, nil)
return
}
switch r.URL.Path {
case "/auth/logout":
if r.Method != "POST" {
response(w, 405, nil)
return
}
delete(f.sessions, token)
response(w, 200, map[string]bool{"loggedOut": true})
case "/system/user/getInfo":
permissions := []string{"vehicle:platform:access", "vehiclePlatform:vehicles:view", "vehiclePlatform:monitor:view", "vehiclePlatform:tracks:view", "vehiclePlatform:statistics:view", "vehiclePlatform:vehicle:read", "vehiclePlatform:track:read", "vehiclePlatform:statistics:read"}
if u.NoAccess {
permissions = []string{}
}
response(w, 200, map[string]any{"user": map[string]any{"userId": u.ID, "tenantId": tenantID, "userName": u.Username, "nickName": u.Name, "deptId": u.ID, "deptName": "本地测试部门", "status": "0"}, "permissions": permissions, "roles": []string{"local-business-user"}})
case "/asset/vehicle-platform/scope":
vehicles := []map[string]any{}
if u.VIN != "" && !u.NoAccess {
vehicles = append(vehicles, map[string]any{"vin": u.VIN, "validFrom": "2026-01-01T00:00:00Z"})
}
response(w, 200, map[string]any{"userId": u.ID, "tenantId": tenantID, "permissionVersion": "fixture-v1", "expiresAt": time.Now().Add(30 * time.Second), "vehicles": vehicles})
}
return
}
if r.URL.Path == "/__local/control" && r.Method == "POST" {
if r.Header.Get("Authorization") != "Bearer "+f.serviceToken {
response(w, 401, nil)
return
}
var input struct{ Username, Action string }
if json.NewDecoder(io.LimitReader(r.Body, 2048)).Decode(&input) != nil || f.users[input.Username] == nil {
response(w, 400, nil)
return
}
u := f.users[input.Username]
switch input.Action {
case "disable":
u.Disabled = true
case "enable":
u.Disabled = false
case "revoke-vehicles":
u.VIN = ""
case "restore-vehicles":
if u.Username == "alpha" {
u.VIN = "LB9A32A24R0LS1426"
} else if u.Username == "beta" {
u.VIN = "LNXNEGRR7SR318212"
}
default:
response(w, 400, nil)
return
}
response(w, 200, map[string]bool{"localOnly": true})
return
}
if r.URL.Path == "/portal/login" && r.Method == "POST" {
if r.ParseForm() != nil || r.FormValue("password") != password || f.users[r.FormValue("username")] == nil {
http.Error(w, "本地测试账号或密码错误", 401)
return
}
token, _ := f.issue(r.FormValue("username"))
http.SetCookie(w, &http.Cookie{Name: "fixture_oneos", Value: token, HttpOnly: true, SameSite: http.SameSiteLaxMode, Path: "/"})
http.Redirect(w, r, "/", 303)
return
}
if r.URL.Path == "/portal/ticket" && r.Method == "POST" {
cookie, e := r.Cookie("fixture_oneos")
if e != nil {
http.Error(w, "请先登录本地模拟 OneOS", 401)
return
}
s, ok := f.sessions[cookie.Value]
if !ok || !s.ExpiresAt.After(time.Now()) {
http.Error(w, "本地会话已失效", 401)
return
}
code := randomToken()
f.tickets[code] = ticket{Token: cookie.Value, ExpiresAt: time.Now().Add(5 * time.Second)}
http.Redirect(w, r, f.platformOrigin+"/api/v2/auth/oneos/callback?code="+url.QueryEscape(code), 303)
return
}
if r.URL.Path == "/" && r.Method == "GET" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
name := ""
if c, e := r.Cookie("fixture_oneos"); e == nil {
if s, ok := f.sessions[c.Value]; ok && s.ExpiresAt.After(time.Now()) {
name = s.User
}
}
_ = portal.Execute(w, map[string]string{"Name": name})
return
}
response(w, 404, nil)
}
var portal = template.Must(template.New("portal").Parse(`<!doctype html><html lang="zh"><meta charset="utf-8"><title>本地 OneOS 模拟器</title><style>body{font:16px system-ui;max-width:640px;margin:80px auto;background:#f4f6fa}main{background:white;padding:36px;border-radius:20px}label{display:block;margin:18px 0}input,button{padding:12px;font:inherit}button{cursor:pointer}small{color:#596579}</style><main><h1>本地 OneOS 模拟器</h1><p>仅回环地址 · 虚构账号 · 不连接线上</p>{{if .Name}}<p>当前 OneOS 用户:<strong>{{.Name}}</strong></p><form method="post" action="/portal/ticket"><button>使用一次性 Ticket 进入中台</button></form>{{end}}<form method="post" action="/portal/login"><label>账号 <input name="username" value="alpha" required></label><label>密码 <input name="password" type="password" value="LocalOnly123!" required></label><button>登录本地 OneOS</button></form><p><small>测试账号 alpha / beta / noaccess,密码 LocalOnly123!。前两个账号分别授权一辆不同车辆;第三个没有中台入口权限。</small></p></main></html>`))
func (f *fixture) decryptLogin(r *http.Request) (map[string]string, error) {
wrapper, e := base64.StdEncoding.DecodeString(r.Header.Get("encrypt-key"))
if e != nil {
return nil, e
}
encoded, e := rsa.DecryptPKCS1v15(rand.Reader, f.key, wrapper)
if e != nil {
return nil, e
}
key, e := base64.StdEncoding.DecodeString(string(encoded))
if e != nil {
return nil, e
}
raw, e := io.ReadAll(io.LimitReader(r.Body, 8193))
if e != nil || len(raw) > 8192 {
return nil, fmt.Errorf("bad body")
}
cipher, e := base64.StdEncoding.DecodeString(string(raw))
if e != nil || len(cipher) == 0 || len(cipher)%aes.BlockSize != 0 {
return nil, fmt.Errorf("bad cipher")
}
block, e := aes.NewCipher(key)
if e != nil {
return nil, e
}
plain := make([]byte, len(cipher))
for i := 0; i < len(cipher); i += aes.BlockSize {
block.Decrypt(plain[i:i+aes.BlockSize], cipher[i:i+aes.BlockSize])
}
pad := int(plain[len(plain)-1])
if pad < 1 || pad > aes.BlockSize || pad > len(plain) {
return nil, fmt.Errorf("bad padding")
}
for _, b := range plain[len(plain)-pad:] {
if int(b) != pad {
return nil, fmt.Errorf("bad padding")
}
}
var input map[string]string
e = json.Unmarshal(plain[:len(plain)-pad], &input)
return input, e
}