package config import ( "encoding/json" "fmt" "os" "path/filepath" ) // AutoConnectRule defines a rule for automatic device connection type AutoConnectRule struct { BusID string `json:"bus_id,omitempty"` VendorID string `json:"vendor_id,omitempty"` ProductID string `json:"product_id,omitempty"` ClientName string `json:"client_name,omitempty"` } // Config holds the client configuration type Config struct { RelayAddr string `json:"relay_addr"` // e.g. "ws://localhost:8443" or "wss://relay.example.com:8443" Hash string `json:"hash"` // SHA256 hash of 3 tokens Mode string `json:"mode"` // "share", "use" or "both" Name string `json:"name"` // friendly name for this client WebPort int `json:"web_port"` // web UI port (default 8080) // Tokens (optional, stored for convenience - hash is what matters) Token1 string `json:"token1,omitempty"` Token2 string `json:"token2,omitempty"` Token3 string `json:"token3,omitempty"` // Auto-connect rules (use mode only) AutoConnect []AutoConnectRule `json:"auto_connect,omitempty"` // Share mode: allow other clients to force-detach devices in use AllowForceDetach bool `json:"allow_force_detach,omitempty"` // DirectPort is the TCP port to accept direct tunnel connections on. // 0 picks a free port, which is fine when peers can reach each other // directly. Set a fixed port when you need to forward it through a // firewall or NAT. DirectPort int `json:"direct_port,omitempty"` // DisableDirect forces every tunnel through the relay. Direct connections // are preferred otherwise: they cut latency and keep USB traffic away // from the relay entirely. DisableDirect bool `json:"disable_direct,omitempty"` // BridgeSocket is a Unix socket path on which to accept USB devices // handed in by another process. Needed where this process cannot open // devices itself — an Android app must obtain the descriptor through the // framework and pass it in. Empty disables the bridge. BridgeSocket string `json:"bridge_socket,omitempty"` } // HasTokens reports whether the full token set is available. // // Tunnel encryption and direct connections both need the tokens themselves; // a config carrying only the group hash can join a group but not derive the // keys, because the hash is what the relay is told. func (c *Config) HasTokens() bool { return c.Token1 != "" && c.Token2 != "" && c.Token3 != "" } // DefaultConfig returns a config with sensible defaults func DefaultConfig() *Config { hostname, _ := os.Hostname() if hostname == "" { hostname = "unknown" } return &Config{ RelayAddr: "ws://localhost:8443", Mode: "use", Name: hostname, WebPort: 8080, } } // DefaultConfigPath returns the default config file path func DefaultConfigPath() string { home, err := os.UserHomeDir() if err != nil { return "usb-client.json" } return filepath.Join(home, ".usb-server", "config.json") } // Load reads config from a JSON file func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config: %w", err) } cfg := DefaultConfig() if err := json.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } return cfg, nil } // Save writes config to a JSON file func (c *Config) Save(path string) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0700); err != nil { return fmt.Errorf("creating config directory: %w", err) } data, err := json.MarshalIndent(c, "", " ") if err != nil { return fmt.Errorf("encoding config: %w", err) } if err := os.WriteFile(path, data, 0600); err != nil { return fmt.Errorf("writing config: %w", err) } return nil }