feat(k1): stabilize LAB bridge and isolate onboard device integration
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EnrollmentCommand struct {
|
||||
ID string `json:"operation_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
RuntimeID string `json:"runtime_id"`
|
||||
Action string `json:"action"`
|
||||
Deadline string `json:"deadline_at"`
|
||||
Parameters map[string]any `json:"parameters"`
|
||||
}
|
||||
|
||||
// This is the complete journal schema. No command parameters or credentials.
|
||||
type EnrollmentOperation struct {
|
||||
ID string `json:"operation_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
RuntimeID string `json:"runtime_id"`
|
||||
Action string `json:"action"`
|
||||
State string `json:"state"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Result map[string]any `json:"result,omitempty"`
|
||||
Remote bool `json:"remote,omitempty"`
|
||||
Updated int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type DeviceEnrollment struct {
|
||||
mu sync.Mutex
|
||||
root string
|
||||
nodeID string
|
||||
client *http.Client
|
||||
operations map[string]*EnrollmentOperation
|
||||
}
|
||||
|
||||
func OpenDeviceEnrollment(root, nodeID string) (*DeviceEnrollment, error) {
|
||||
dir := filepath.Join(root, "device-enrollment")
|
||||
if e := os.MkdirAll(dir, 0700); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
d := &DeviceEnrollment{root: dir, nodeID: nodeID, operations: map[string]*EnrollmentOperation{}}
|
||||
d.client = &http.Client{Timeout: 185 * time.Second, Transport: &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, "unix", "/run/mission-core-k1/driver.sock")
|
||||
}}}
|
||||
files, _ := filepath.Glob(filepath.Join(dir, "op_*.json"))
|
||||
for _, path := range files {
|
||||
data, e := os.ReadFile(path)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
var op EnrollmentOperation
|
||||
if json.Unmarshal(data, &op) != nil || !operationID.MatchString(op.ID) {
|
||||
return nil, errors.New("invalid enrollment journal")
|
||||
}
|
||||
if op.State == "running" {
|
||||
op.State = "unknown"
|
||||
op.Error = "БК перезапущен. Обновите состояние K1 перед новой командой."
|
||||
}
|
||||
d.operations[op.ID] = &op
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *DeviceEnrollment) call(ctx context.Context, path string, command any) (map[string]any, error) {
|
||||
var body io.Reader
|
||||
method := "GET"
|
||||
if command != nil {
|
||||
data, e := json.Marshal(command)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
body = bytes.NewReader(data)
|
||||
method = "POST"
|
||||
}
|
||||
request, e := http.NewRequestWithContext(ctx, method, "http://k1"+path, body)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("X-Node-Id", d.nodeID)
|
||||
response, e := d.client.Do(request)
|
||||
if e != nil {
|
||||
return nil, errors.New("Служба K1 на БК недоступна.")
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var value map[string]any
|
||||
if json.NewDecoder(io.LimitReader(response.Body, 65536)).Decode(&value) != nil || response.StatusCode != 200 {
|
||||
return nil, errors.New("Действие K1 не подтверждено. Обновите состояние устройства.")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (d *DeviceEnrollment) Status() map[string]any {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
value, e := d.call(ctx, "/status", nil)
|
||||
if e != nil {
|
||||
value = map[string]any{"available": false}
|
||||
}
|
||||
value["node_id"] = d.nodeID
|
||||
value["name"], _ = os.Hostname()
|
||||
value["fresh"] = true
|
||||
return value
|
||||
}
|
||||
|
||||
func (c EnrollmentCommand) valid(nodeID string) bool {
|
||||
deadline, e := time.Parse(time.RFC3339Nano, c.Deadline)
|
||||
if e != nil || !deadline.After(time.Now()) || time.Until(deadline) > 180*time.Second || c.NodeID != nodeID || !operationID.MatchString(c.ID) || c.RuntimeID == "" || len(c.RuntimeID) > 128 {
|
||||
return false
|
||||
}
|
||||
keys := map[string]bool{}
|
||||
switch c.Action {
|
||||
case "scan", "networks":
|
||||
case "connect", "verify":
|
||||
keys = map[string]bool{"device_id": true, "discovery_generation": true, "mode_revision": true}
|
||||
id, ok := c.Parameters["device_id"].(string)
|
||||
if !ok || len(id) < 1 || len(id) > 128 {
|
||||
return false
|
||||
}
|
||||
for _, key := range []string{"discovery_generation", "mode_revision"} {
|
||||
v, ok := c.Parameters[key].(float64)
|
||||
if !ok || v < 0 || v != float64(int64(v)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if c.Action == "connect" {
|
||||
keys["ssid"] = true
|
||||
keys["password"] = true
|
||||
for key, limit := range map[string]int{"ssid": 32, "password": 64} {
|
||||
v, ok := c.Parameters[key].(string)
|
||||
if !ok || len(v) < 1 || len(v) > limit {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if len(keys) != len(c.Parameters) {
|
||||
return false
|
||||
}
|
||||
for key := range c.Parameters {
|
||||
if !keys[key] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func copyEnrollment(value *EnrollmentOperation) *EnrollmentOperation {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
data, _ := json.Marshal(value)
|
||||
var out EnrollmentOperation
|
||||
_ = json.Unmarshal(data, &out)
|
||||
return &out
|
||||
}
|
||||
|
||||
func (d *DeviceEnrollment) Submit(c EnrollmentCommand, remote bool) (*EnrollmentOperation, error) {
|
||||
if !c.valid(d.nodeID) {
|
||||
return nil, errors.New("Проверьте БК, выбранное устройство и параметры сети.")
|
||||
}
|
||||
d.mu.Lock()
|
||||
if old := d.operations[c.ID]; old != nil {
|
||||
out := copyEnrollment(old)
|
||||
d.mu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
if len(d.operations) >= 2000 {
|
||||
for id, value := range d.operations {
|
||||
if value.State != "running" && time.Now().Unix()-value.Updated > 86400 {
|
||||
if os.Remove(filepath.Join(d.root, id+".json")) == nil {
|
||||
delete(d.operations, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(d.operations) >= 2000 {
|
||||
d.mu.Unlock()
|
||||
return nil, errors.New("Журнал подключений заполнен. Повторите позже.")
|
||||
}
|
||||
}
|
||||
for _, v := range d.operations {
|
||||
if v.State == "running" {
|
||||
d.mu.Unlock()
|
||||
return nil, errors.New("Дождитесь текущего действия K1.")
|
||||
}
|
||||
}
|
||||
d.mu.Unlock()
|
||||
if d.Status()["runtime_id"] != c.RuntimeID {
|
||||
return nil, errors.New("Сеанс K1 изменился. Обновите устройства.")
|
||||
}
|
||||
d.mu.Lock()
|
||||
// Status can yield; repeat admission under the lock before the durable write.
|
||||
if old := d.operations[c.ID]; old != nil {
|
||||
out := copyEnrollment(old)
|
||||
d.mu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
for _, v := range d.operations {
|
||||
if v.State == "running" {
|
||||
d.mu.Unlock()
|
||||
return nil, errors.New("Дождитесь текущего действия K1.")
|
||||
}
|
||||
}
|
||||
op := &EnrollmentOperation{ID: c.ID, NodeID: c.NodeID, RuntimeID: c.RuntimeID, Action: c.Action, State: "running", Remote: remote, Updated: time.Now().Unix()}
|
||||
if e := savePrivateJSON(filepath.Join(d.root, c.ID+".json"), op); e != nil {
|
||||
d.mu.Unlock()
|
||||
return nil, e
|
||||
}
|
||||
d.operations[c.ID] = op
|
||||
out := copyEnrollment(op)
|
||||
d.mu.Unlock()
|
||||
go d.execute(c)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DeviceEnrollment) execute(c EnrollmentCommand) {
|
||||
deadline, _ := time.Parse(time.RFC3339Nano, c.Deadline)
|
||||
ctx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
result, e := d.call(ctx, "/operation", c)
|
||||
// Dropping references is best-effort lifetime reduction, not a claim that
|
||||
// Go can zero every immutable JSON/string copy. No secret is journalled.
|
||||
delete(c.Parameters, "password")
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
op := d.operations[c.ID]
|
||||
op.Updated = time.Now().Unix()
|
||||
op.State = "complete"
|
||||
op.Result = result
|
||||
if e != nil {
|
||||
op.State = "unknown"
|
||||
op.Error = "Действие K1 не подтверждено. Обновите состояние; повторная команда автоматически не отправляется."
|
||||
}
|
||||
if e := savePrivateJSON(filepath.Join(d.root, c.ID+".json"), op); e != nil {
|
||||
op.State = "unknown"
|
||||
op.Error = "Результат не сохранён. Обновите состояние K1."
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeviceEnrollment) Get(id string) *EnrollmentOperation {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return copyEnrollment(d.operations[id])
|
||||
}
|
||||
func (d *DeviceEnrollment) RemoteResults() []*EnrollmentOperation {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
out := []*EnrollmentOperation{}
|
||||
for _, op := range d.operations {
|
||||
if op.Remote {
|
||||
out = append(out, copyEnrollment(op))
|
||||
if len(out) == 8 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
func (d *DeviceEnrollment) Acknowledge(ids []string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
for _, id := range ids {
|
||||
if op := d.operations[id]; op != nil && op.State != "running" {
|
||||
op.Remote = false
|
||||
_ = savePrivateJSON(filepath.Join(d.root, id+".json"), op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeviceEnrollment) Routes(mux *http.ServeMux, server *Server) {
|
||||
mux.HandleFunc("GET /api/devices/enrollment", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !server.authorized(w, r) {
|
||||
return
|
||||
}
|
||||
v := d.Status()
|
||||
_, name := server.Store.Public()
|
||||
v["name"] = name
|
||||
reply(w, 200, v)
|
||||
})
|
||||
mux.HandleFunc("POST /api/devices/enrollment/operations", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !server.authorized(w, r) {
|
||||
return
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 16384)
|
||||
var c EnrollmentCommand
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
if r.Header.Get("Content-Type") != "application/json" || decoder.Decode(&c) != nil {
|
||||
reply(w, 400, map[string]string{"error": "Некорректный запрос подключения"})
|
||||
return
|
||||
}
|
||||
op, e := d.Submit(c, false)
|
||||
if e != nil {
|
||||
reply(w, 409, map[string]string{"error": e.Error()})
|
||||
return
|
||||
}
|
||||
reply(w, 202, op)
|
||||
})
|
||||
mux.HandleFunc("GET /api/devices/enrollment/operations/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !server.authorized(w, r) {
|
||||
return
|
||||
}
|
||||
op := d.Get(r.PathValue("id"))
|
||||
if op == nil {
|
||||
reply(w, 200, map[string]string{"state": "unknown", "error": "Результат недоступен. Обновите состояние K1."})
|
||||
return
|
||||
}
|
||||
reply(w, 200, op)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type enrollmentHTTP func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f enrollmentHTTP) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
||||
|
||||
func enrollmentRequest() EnrollmentCommand {
|
||||
return EnrollmentCommand{ID: "op_" + strings.Repeat("a", 32), NodeID: "node-test", RuntimeID: "runtime-test", Action: "connect", Deadline: time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), Parameters: map[string]any{"device_id": "test-ble", "mode_revision": float64(0), "discovery_generation": float64(1), "ssid": "test-network", "password": token()}}
|
||||
}
|
||||
|
||||
func TestEnrollmentJournalsNoCredentialAndDispatchesOnce(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
d, e := OpenDeviceEnrollment(root, "node-test")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
var posts atomic.Int32
|
||||
d.client = &http.Client{Transport: enrollmentHTTP(func(r *http.Request) (*http.Response, error) {
|
||||
body := `{"available":true,"runtime_id":"runtime-test"}`
|
||||
if r.Method == "POST" {
|
||||
posts.Add(1)
|
||||
body = `{"available":true,"connected":true}`
|
||||
}
|
||||
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
|
||||
})}
|
||||
c := enrollmentRequest()
|
||||
secret := c.Parameters["password"].(string)
|
||||
if _, e = d.Submit(c, true); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for d.Get(c.ID).State == "running" && time.Now().Before(deadline) {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
if d.Get(c.ID).State != "complete" {
|
||||
t.Fatal("operation did not complete")
|
||||
}
|
||||
// A repeated delivery owns no new physical intent, even with a replaced secret.
|
||||
c.Parameters["password"] = token()
|
||||
if _, e = d.Submit(c, true); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if posts.Load() != 1 {
|
||||
t.Fatal("duplicate dispatch")
|
||||
}
|
||||
journal, e := os.ReadFile(filepath.Join(root, "device-enrollment", c.ID+".json"))
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
results, _ := json.Marshal(d.RemoteResults())
|
||||
if bytes.Contains(journal, []byte(secret)) || bytes.Contains(results, []byte(secret)) || bytes.Contains(journal, []byte("parameters")) {
|
||||
t.Fatal("credential/parameters entered journal or results")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrollmentRestartDoesNotReplayRunningIntent(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
d, e := OpenDeviceEnrollment(root, "node-test")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
c := enrollmentRequest()
|
||||
op := EnrollmentOperation{ID: c.ID, NodeID: c.NodeID, RuntimeID: c.RuntimeID, Action: c.Action, State: "running", Remote: true}
|
||||
if e := savePrivateJSON(filepath.Join(d.root, c.ID+".json"), op); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
restarted, e := OpenDeviceEnrollment(root, "node-test")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if restarted.Get(c.ID).State != "unknown" {
|
||||
t.Fatal("restart did not fence unknown result")
|
||||
}
|
||||
restarted.client = &http.Client{Transport: enrollmentHTTP(func(*http.Request) (*http.Response, error) { t.Fatal("restart contacted hardware"); return nil, nil })}
|
||||
result, e := restarted.Submit(c, true)
|
||||
if e != nil || result.State != "unknown" {
|
||||
t.Fatal("repeated intent did not retain unknown outcome")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrollmentRejectsAnotherNodeExpiredAndHostMutation(t *testing.T) {
|
||||
c := enrollmentRequest()
|
||||
if !c.valid("node-test") {
|
||||
t.Fatal("valid intent rejected")
|
||||
}
|
||||
if c.valid("another-node") {
|
||||
t.Fatal("wrong node admitted")
|
||||
}
|
||||
c.Parameters["allow_host_wifi_switch"] = true
|
||||
if c.valid("node-test") {
|
||||
t.Fatal("host association admitted")
|
||||
}
|
||||
delete(c.Parameters, "allow_host_wifi_switch")
|
||||
c.Deadline = time.Now().Add(-time.Second).Format(time.RFC3339Nano)
|
||||
if c.valid("node-test") {
|
||||
t.Fatal("expired operation admitted")
|
||||
}
|
||||
}
|
||||
@@ -39,20 +39,21 @@ type PairState struct {
|
||||
Revocations []CoreBinding `json:"revocations,omitempty"`
|
||||
}
|
||||
type Pairing struct {
|
||||
Sensors *Sensors
|
||||
mu sync.Mutex
|
||||
path string
|
||||
store *Store
|
||||
state PairState
|
||||
now func() time.Time
|
||||
inventory func() Inventory
|
||||
version string
|
||||
lastSeen int64
|
||||
connection string
|
||||
listenError string
|
||||
failureWindow int64
|
||||
failures int
|
||||
clients map[string]*http.Client
|
||||
Sensors *Sensors
|
||||
DeviceEnrollment *DeviceEnrollment
|
||||
mu sync.Mutex
|
||||
path string
|
||||
store *Store
|
||||
state PairState
|
||||
now func() time.Time
|
||||
inventory func() Inventory
|
||||
version string
|
||||
lastSeen int64
|
||||
connection string
|
||||
listenError string
|
||||
failureWindow int64
|
||||
failures int
|
||||
clients map[string]*http.Client
|
||||
}
|
||||
|
||||
func OpenPairing(store *Store, dir, version string, inventory func() Inventory) (*Pairing, error) {
|
||||
|
||||
@@ -282,12 +282,28 @@ func (p *Pairing) channel(ctx context.Context) {
|
||||
payload["sensor_state"] = inv
|
||||
payload["sensor_results"] = p.Sensors.RemoteResults()
|
||||
}
|
||||
if p.DeviceEnrollment != nil {
|
||||
payload["device_enrollment"] = p.DeviceEnrollment.Status()
|
||||
payload["enrollment_results"] = p.DeviceEnrollment.RemoteResults()
|
||||
}
|
||||
result, status, e := p.send(ctx, *binding, "/v1/node/heartbeat", payload)
|
||||
p.mu.Lock()
|
||||
if p.state.Phase == "paired" && p.state.Binding != nil && p.state.Binding.BindingID == binding.BindingID {
|
||||
if e == nil && status == 200 {
|
||||
p.connection = "online"
|
||||
p.lastSeen = p.now().Unix()
|
||||
if p.DeviceEnrollment != nil {
|
||||
var ack []string
|
||||
if json.Unmarshal(result["enrollment_acknowledgements"], &ack) == nil {
|
||||
p.DeviceEnrollment.Acknowledge(ack)
|
||||
}
|
||||
var commands []EnrollmentCommand
|
||||
if json.Unmarshal(result["enrollment_commands"], &commands) == nil {
|
||||
for _, c := range commands {
|
||||
_, _ = p.DeviceEnrollment.Submit(c, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.Sensors != nil {
|
||||
var ack []string
|
||||
if json.Unmarshal(result["sensor_acknowledgements"], &ack) == nil {
|
||||
|
||||
@@ -45,19 +45,20 @@ type SensorOperation struct {
|
||||
Updated int64 `json:"updated_at"`
|
||||
}
|
||||
type Sensors struct {
|
||||
events sensorEvents
|
||||
mu sync.Mutex
|
||||
prepareMu sync.Mutex
|
||||
root string
|
||||
nodeID string
|
||||
instance string
|
||||
client *http.Client
|
||||
operations map[string]*SensorOperation
|
||||
names map[string]string
|
||||
initialized map[string]bool
|
||||
events sensorEvents
|
||||
mu sync.Mutex
|
||||
prepareMu sync.Mutex
|
||||
root string
|
||||
nodeID string
|
||||
instance string
|
||||
client *http.Client
|
||||
NetworkDevices *DeviceEnrollment
|
||||
operations map[string]*SensorOperation
|
||||
names map[string]string
|
||||
initialized map[string]bool
|
||||
}
|
||||
|
||||
var sensorID = regexp.MustCompile(`^rsd455_[0-9a-f]{32}$`)
|
||||
var sensorID = regexp.MustCompile(`^(rsd455|k1)_[0-9a-f]{32}$`)
|
||||
var operationID = regexp.MustCompile(`^op_[0-9a-f]{32}$`)
|
||||
|
||||
func OpenSensors(root, nodeID string) (*Sensors, error) {
|
||||
@@ -168,6 +169,32 @@ func (s *Sensors) driver(path string, body any) (map[string]any, error) {
|
||||
func (s *Sensors) Inventory() map[string]any {
|
||||
items := []any{}
|
||||
seen := map[string]bool{}
|
||||
if s.NetworkDevices != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
result, e := s.NetworkDevices.call(ctx, "/inventory", nil)
|
||||
cancel()
|
||||
if e == nil {
|
||||
if found, ok := result["items"].([]any); ok {
|
||||
for _, raw := range found {
|
||||
item, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, _ := item["id"].(string)
|
||||
if !strings.HasPrefix(id, "k1_") || !sensorID.MatchString(id) {
|
||||
continue
|
||||
}
|
||||
s.mu.Lock()
|
||||
if name := s.names[id]; name != "" {
|
||||
item["name"] = name
|
||||
}
|
||||
s.mu.Unlock()
|
||||
seen[id] = true
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if result, e := s.driver("/inventory", nil); e == nil {
|
||||
if found, ok := result["items"].([]any); ok {
|
||||
for _, v := range found {
|
||||
@@ -259,6 +286,9 @@ func sensorViewAction(action string) bool {
|
||||
}
|
||||
|
||||
func (s *Sensors) Submit(c SensorCommand, remote bool) (*SensorOperation, error) {
|
||||
if strings.HasPrefix(c.Session.DeviceID, "k1_") && (c.Action == "prepare" || c.Action == "replay") {
|
||||
return nil, errors.New("Эта операция не поддерживается K1.")
|
||||
}
|
||||
if c.APIVersion != SensorSchema || c.Kind != "OperationRequest" || !operationID.MatchString(c.ID) || c.Idempotency != c.ID || !sensorID.MatchString(c.Session.DeviceID) || len(c.Session.SessionID) > 192 {
|
||||
return nil, errors.New("Некорректная команда устройства.")
|
||||
}
|
||||
@@ -344,7 +374,14 @@ func (s *Sensors) execute(c SensorCommand) {
|
||||
}
|
||||
} else {
|
||||
var v map[string]any
|
||||
v, err = s.driver("/operation", c)
|
||||
if strings.HasPrefix(c.Session.DeviceID, "k1_") && s.NetworkDevices != nil {
|
||||
deadline, _ := time.Parse(time.RFC3339Nano, c.Deadline)
|
||||
ctx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
v, err = s.NetworkDevices.call(ctx, "/sensor-operation", c)
|
||||
cancel()
|
||||
} else {
|
||||
v, err = s.driver("/operation", c)
|
||||
}
|
||||
uncertain = err != nil || v["state"] == "unknown"
|
||||
if err == nil {
|
||||
if v["state"] == "complete" {
|
||||
|
||||
@@ -13,20 +13,21 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Store *Store
|
||||
Pairing *Pairing
|
||||
Sensors *Sensors
|
||||
Assets fs.FS
|
||||
Origin string
|
||||
Version string
|
||||
Inventory func() Inventory
|
||||
Access *AccessStore
|
||||
Tailscale func() TailscaleStatus
|
||||
Environment func() EnvironmentStatus
|
||||
mu sync.Mutex
|
||||
logins map[string]time.Time
|
||||
sessions map[string]time.Time
|
||||
Now func() time.Time
|
||||
Store *Store
|
||||
Pairing *Pairing
|
||||
Sensors *Sensors
|
||||
DeviceEnrollment *DeviceEnrollment
|
||||
Assets fs.FS
|
||||
Origin string
|
||||
Version string
|
||||
Inventory func() Inventory
|
||||
Access *AccessStore
|
||||
Tailscale func() TailscaleStatus
|
||||
Environment func() EnvironmentStatus
|
||||
mu sync.Mutex
|
||||
logins map[string]time.Time
|
||||
sessions map[string]time.Time
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
func token() string {
|
||||
@@ -82,6 +83,9 @@ func (s *Server) Handler() http.Handler {
|
||||
if s.Sensors != nil {
|
||||
s.Sensors.Routes(mux, s)
|
||||
}
|
||||
if s.DeviceEnrollment != nil {
|
||||
s.DeviceEnrollment.Routes(mux, s)
|
||||
}
|
||||
if s.Pairing != nil {
|
||||
s.Pairing.localRoutes(mux, s)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user