// 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(`本地 OneOS 模拟器

本地 OneOS 模拟器

仅回环地址 · 虚构账号 · 不连接线上

{{if .Name}}

当前 OneOS 用户:{{.Name}}

{{end}}

测试账号 alpha / beta / noaccess,密码 LocalOnly123!。前两个账号分别授权一辆不同车辆;第三个没有中台入口权限。

`)) 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 }