first commit
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
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)
|
||||
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,
|
||||
"client_id": d.ClientID,
|
||||
"client_name": d.ClientName,
|
||||
"tunnel_id": d.TunnelID,
|
||||
"vhci_port": d.VHCIPort,
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"mode": "use",
|
||||
"available_devices": availList,
|
||||
"attached_devices": attachList,
|
||||
}
|
||||
}
|
||||
webHandler.AttachDevice = um.AttachDevice
|
||||
webHandler.DetachDevice = um.DetachDevice
|
||||
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)
|
||||
}
|
||||
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] + "..."
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/duffy/usb-server/internal/relay"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := flag.Int("port", 8443, "listen port")
|
||||
addr := flag.String("addr", "0.0.0.0", "listen address")
|
||||
flag.Parse()
|
||||
|
||||
// Also check environment variables (for Docker)
|
||||
if envPort := os.Getenv("RELAY_PORT"); envPort != "" {
|
||||
fmt.Sscanf(envPort, "%d", port)
|
||||
}
|
||||
if envAddr := os.Getenv("RELAY_ADDR"); envAddr != "" {
|
||||
*addr = envAddr
|
||||
}
|
||||
|
||||
listenAddr := fmt.Sprintf("%s:%d", *addr, *port)
|
||||
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.Printf("USB Relay Server starting on %s", listenAddr)
|
||||
|
||||
server := relay.NewServer(listenAddr)
|
||||
if err := server.Run(); err != nil {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user