443 lines
10 KiB
Go
443 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/duffy/usb-server/internal/client"
|
|
"github.com/duffy/usb-server/internal/config"
|
|
"github.com/duffy/usb-server/internal/service"
|
|
"github.com/duffy/usb-server/internal/token"
|
|
"github.com/duffy/usb-server/internal/usb"
|
|
"github.com/duffy/usb-server/internal/web"
|
|
)
|
|
|
|
func main() {
|
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
|
|
if len(os.Args) < 2 {
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
switch os.Args[1] {
|
|
case "generate-token":
|
|
cmdGenerateToken()
|
|
case "share":
|
|
cmdRun("share")
|
|
case "use":
|
|
cmdRun("use")
|
|
case "list":
|
|
cmdList()
|
|
case "gui":
|
|
cmdGUI()
|
|
case "config":
|
|
cmdConfig()
|
|
case "install-service":
|
|
cmdInstallService()
|
|
case "uninstall-service":
|
|
cmdUninstallService()
|
|
case "help", "-h", "--help":
|
|
printUsage()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func printUsage() {
|
|
fmt.Println(`USB Client - USB over IP
|
|
|
|
Usage:
|
|
usb-client <command> [options]
|
|
|
|
Commands:
|
|
generate-token Generate 3 tokens and compute hash
|
|
share Start in share mode (expose USB devices)
|
|
use Start in use mode (consume USB devices)
|
|
list List local USB devices
|
|
gui Start web UI only
|
|
config Show current configuration
|
|
install-service Install as systemd service
|
|
uninstall-service Remove systemd service
|
|
help Show this help
|
|
|
|
Options:
|
|
--config <path> Config file path (default: ~/.usb-server/config.json)
|
|
--relay <addr> Relay server address (e.g. ws://localhost:8443)
|
|
--hash <hash> Group hash
|
|
--name <name> Client name
|
|
--web-port <port> Web UI port (default: 8080)
|
|
--no-gui Disable web UI`)
|
|
}
|
|
|
|
func loadConfig() (*config.Config, string) {
|
|
cfgPath := config.DefaultConfigPath()
|
|
|
|
// Check for --config flag in os.Args
|
|
for i, arg := range os.Args {
|
|
if arg == "--config" && i+1 < len(os.Args) {
|
|
cfgPath = os.Args[i+1]
|
|
}
|
|
}
|
|
|
|
cfg, err := config.Load(cfgPath)
|
|
if err != nil {
|
|
cfg = config.DefaultConfig()
|
|
}
|
|
|
|
// Override with flags
|
|
for i := 2; i < len(os.Args); i++ {
|
|
switch os.Args[i] {
|
|
case "--relay":
|
|
if i+1 < len(os.Args) {
|
|
cfg.RelayAddr = os.Args[i+1]
|
|
i++
|
|
}
|
|
case "--hash":
|
|
if i+1 < len(os.Args) {
|
|
cfg.Hash = os.Args[i+1]
|
|
i++
|
|
}
|
|
case "--name":
|
|
if i+1 < len(os.Args) {
|
|
cfg.Name = os.Args[i+1]
|
|
i++
|
|
}
|
|
case "--web-port":
|
|
if i+1 < len(os.Args) {
|
|
fmt.Sscanf(os.Args[i+1], "%d", &cfg.WebPort)
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
return cfg, cfgPath
|
|
}
|
|
|
|
func cmdGenerateToken() {
|
|
tokens, err := token.Generate()
|
|
if err != nil {
|
|
log.Fatalf("Error generating tokens: %v", err)
|
|
}
|
|
|
|
hash := tokens.Hash()
|
|
|
|
fmt.Println("Generated Tokens:")
|
|
fmt.Println("=================")
|
|
fmt.Printf("Token 1: %s\n", tokens.Token1)
|
|
fmt.Printf("Token 2: %s\n", tokens.Token2)
|
|
fmt.Printf("Token 3: %s\n", tokens.Token3)
|
|
fmt.Println()
|
|
fmt.Printf("Hash: %s\n", hash)
|
|
fmt.Println()
|
|
fmt.Println("Copy all 3 tokens to all clients that should share USB devices.")
|
|
fmt.Println("The hash is computed from the tokens and used for grouping.")
|
|
|
|
// Optionally save to config
|
|
cfg, cfgPath := loadConfig()
|
|
cfg.Token1 = tokens.Token1
|
|
cfg.Token2 = tokens.Token2
|
|
cfg.Token3 = tokens.Token3
|
|
cfg.Hash = hash
|
|
if err := cfg.Save(cfgPath); err != nil {
|
|
log.Printf("Warning: could not save config: %v", err)
|
|
} else {
|
|
fmt.Printf("\nTokens saved to %s\n", cfgPath)
|
|
}
|
|
}
|
|
|
|
func cmdRun(mode string) {
|
|
cfg, cfgPath := loadConfig()
|
|
cfg.Mode = mode
|
|
|
|
if cfg.Hash == "" {
|
|
fmt.Println("Error: No hash configured. Run 'usb-client generate-token' first or set --hash.")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check for --no-gui
|
|
noGUI := false
|
|
for _, arg := range os.Args {
|
|
if arg == "--no-gui" {
|
|
noGUI = true
|
|
}
|
|
}
|
|
|
|
// Create client
|
|
c := client.NewClient(cfg)
|
|
|
|
// Setup signal handling
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
// Start web UI unless disabled
|
|
if !noGUI {
|
|
webHandler := web.NewHandler(cfg, cfgPath)
|
|
|
|
if mode == "share" {
|
|
sm := client.NewShareManager(c)
|
|
webHandler.GetDevices = func() interface{} {
|
|
return map[string]interface{}{
|
|
"mode": "share",
|
|
"local_devices": sm.DeviceListForAPI(),
|
|
}
|
|
}
|
|
webHandler.GetStatus = func() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"connected": true, // simplified
|
|
"mode": mode,
|
|
"name": cfg.Name,
|
|
"client_id": c.ID(),
|
|
}
|
|
}
|
|
webHandler.InstallService = func() error { return service.Install(mode, cfgPath) }
|
|
webHandler.UninstallService = service.Uninstall
|
|
|
|
go func() {
|
|
if err := c.Run(); err != nil {
|
|
log.Printf("Client error: %v", err)
|
|
}
|
|
}()
|
|
go sm.Run()
|
|
} else {
|
|
um := client.NewUseManager(c, cfg, cfgPath)
|
|
webHandler.GetDevices = func() interface{} {
|
|
available := um.GetAvailableDevices()
|
|
attached := um.GetAttachedDevices()
|
|
|
|
var availList []map[string]interface{}
|
|
for _, d := range available {
|
|
availList = append(availList, map[string]interface{}{
|
|
"bus_id": d.BusID,
|
|
"vendor_id": d.VendorID,
|
|
"product_id": d.ProductID,
|
|
"name": d.Name,
|
|
"status": d.Status,
|
|
"speed": d.Speed,
|
|
"client_id": d.ClientID,
|
|
"client_name": d.ClientName,
|
|
})
|
|
}
|
|
|
|
var attachList []map[string]interface{}
|
|
for _, d := range attached {
|
|
attachList = append(attachList, map[string]interface{}{
|
|
"bus_id": d.BusID,
|
|
"vendor_id": d.VendorID,
|
|
"product_id": d.ProductID,
|
|
"name": d.Name,
|
|
"client_id": d.ClientID,
|
|
"client_name": d.ClientName,
|
|
"tunnel_id": d.TunnelID,
|
|
"vhci_port": d.VHCIPort,
|
|
"auto_connect": um.IsAutoConnect(d.VendorID, d.ProductID),
|
|
})
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"mode": "use",
|
|
"available_devices": availList,
|
|
"attached_devices": attachList,
|
|
}
|
|
}
|
|
webHandler.AttachDevice = um.AttachDevice
|
|
webHandler.DetachDevice = um.DetachDevice
|
|
webHandler.SetAutoConnect = um.SetAutoConnect
|
|
webHandler.IsAutoConnect = um.IsAutoConnect
|
|
webHandler.GetStatus = func() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"connected": true,
|
|
"mode": mode,
|
|
"name": cfg.Name,
|
|
"client_id": c.ID(),
|
|
}
|
|
}
|
|
webHandler.InstallService = func() error { return service.Install(mode, cfgPath) }
|
|
webHandler.UninstallService = service.Uninstall
|
|
|
|
go func() {
|
|
if err := c.Run(); err != nil {
|
|
log.Printf("Client error: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
addr := fmt.Sprintf(":%d", cfg.WebPort)
|
|
log.Printf("Web UI available at http://localhost%s", addr)
|
|
go func() {
|
|
if err := http.ListenAndServe(addr, webHandler); err != nil {
|
|
log.Printf("Web UI error: %v", err)
|
|
}
|
|
}()
|
|
} else {
|
|
// No GUI mode
|
|
if mode == "share" {
|
|
sm := client.NewShareManager(c)
|
|
go sm.Run()
|
|
} else {
|
|
client.NewUseManager(c, cfg, cfgPath)
|
|
}
|
|
go func() {
|
|
if err := c.Run(); err != nil {
|
|
log.Printf("Client error: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
log.Printf("USB Client started (mode=%s, name=%s)", mode, cfg.Name)
|
|
|
|
// Wait for signal
|
|
sig := <-sigChan
|
|
log.Printf("Received signal %v, shutting down...", sig)
|
|
c.Close()
|
|
}
|
|
|
|
func cmdList() {
|
|
devices, err := usb.Enumerate()
|
|
if err != nil {
|
|
log.Fatalf("Error enumerating USB devices: %v", err)
|
|
}
|
|
|
|
if len(devices) == 0 {
|
|
fmt.Println("No USB devices found.")
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%-10s %-10s %-30s %-8s %s\n", "BUS-ID", "VID:PID", "NAME", "SPEED", "DRIVER")
|
|
fmt.Println(strings.Repeat("-", 80))
|
|
|
|
for _, dev := range devices {
|
|
driver := ""
|
|
if len(dev.Interfaces) > 0 {
|
|
driver = dev.Interfaces[0].Driver
|
|
}
|
|
speedNames := map[uint32]string{
|
|
1: "Low", 2: "Full", 3: "High", 5: "Super", 6: "Super+",
|
|
}
|
|
speed := speedNames[dev.Speed]
|
|
if speed == "" {
|
|
speed = "?"
|
|
}
|
|
|
|
fmt.Printf("%-10s %04x:%04x %-30s %-8s %s\n",
|
|
dev.BusID,
|
|
dev.VendorID, dev.ProductID,
|
|
truncate(dev.DisplayName(), 30),
|
|
speed,
|
|
driver,
|
|
)
|
|
}
|
|
}
|
|
|
|
func cmdGUI() {
|
|
cfg, cfgPath := loadConfig()
|
|
|
|
webHandler := web.NewHandler(cfg, cfgPath)
|
|
webHandler.GetDevices = func() interface{} {
|
|
devices, _ := usb.Enumerate()
|
|
var devList []map[string]interface{}
|
|
for _, d := range devices {
|
|
devList = append(devList, map[string]interface{}{
|
|
"bus_id": d.BusID,
|
|
"vendor_id": fmt.Sprintf("%04x", d.VendorID),
|
|
"product_id": fmt.Sprintf("%04x", d.ProductID),
|
|
"name": d.DisplayName(),
|
|
"status": "available",
|
|
"speed": d.Speed,
|
|
})
|
|
}
|
|
return map[string]interface{}{
|
|
"mode": cfg.Mode,
|
|
"local_devices": devList,
|
|
}
|
|
}
|
|
webHandler.GetStatus = func() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"connected": false,
|
|
"mode": cfg.Mode,
|
|
"name": cfg.Name,
|
|
}
|
|
}
|
|
|
|
addr := fmt.Sprintf(":%d", cfg.WebPort)
|
|
fmt.Printf("Web UI: http://localhost%s\n", addr)
|
|
log.Fatal(http.ListenAndServe(addr, webHandler))
|
|
}
|
|
|
|
func cmdConfig() {
|
|
cfg, cfgPath := loadConfig()
|
|
|
|
// Check for subcommand
|
|
if len(os.Args) > 2 && os.Args[2] == "show" || len(os.Args) == 2 {
|
|
fmt.Printf("Config file: %s\n\n", cfgPath)
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
log.Fatalf("Error encoding config: %v", err)
|
|
}
|
|
fmt.Println(string(data))
|
|
return
|
|
}
|
|
|
|
// Handle set subcommand
|
|
if os.Args[2] == "set" {
|
|
fs := flag.NewFlagSet("config set", flag.ExitOnError)
|
|
relay := fs.String("relay", "", "Relay server address")
|
|
hash := fs.String("hash", "", "Group hash")
|
|
mode := fs.String("mode", "", "Client mode (share/use)")
|
|
name := fs.String("name", "", "Client name")
|
|
fs.Parse(os.Args[3:])
|
|
|
|
if *relay != "" {
|
|
cfg.RelayAddr = *relay
|
|
}
|
|
if *hash != "" {
|
|
cfg.Hash = *hash
|
|
}
|
|
if *mode != "" {
|
|
cfg.Mode = *mode
|
|
}
|
|
if *name != "" {
|
|
cfg.Name = *name
|
|
}
|
|
|
|
if err := cfg.Save(cfgPath); err != nil {
|
|
log.Fatalf("Error saving config: %v", err)
|
|
}
|
|
fmt.Println("Config saved.")
|
|
return
|
|
}
|
|
|
|
printUsage()
|
|
}
|
|
|
|
func cmdInstallService() {
|
|
cfg, cfgPath := loadConfig()
|
|
if err := service.Install(cfg.Mode, cfgPath); err != nil {
|
|
log.Fatalf("Error installing service: %v", err)
|
|
}
|
|
fmt.Println("Service installed and started.")
|
|
}
|
|
|
|
func cmdUninstallService() {
|
|
if err := service.Uninstall(); err != nil {
|
|
log.Fatalf("Error uninstalling service: %v", err)
|
|
}
|
|
fmt.Println("Service uninstalled.")
|
|
}
|
|
|
|
func truncate(s string, max int) string {
|
|
if len(s) <= max {
|
|
return s
|
|
}
|
|
return s[:max-3] + "..."
|
|
}
|