diff --git a/go.mod b/go.mod index 460e8c1..20c5772 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.23.2 require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 require ( + github.com/fosrl/newt v0.0.0-20250215225251-76503f3f2cd8 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect diff --git a/go.sum b/go.sum index f453d4f..e1f8390 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/fosrl/newt v0.0.0-20250215225251-76503f3f2cd8 h1:SEZcXV/++6+YMPkSXa59EevKSz1ZxmNDd04amswFPqE= +github.com/fosrl/newt v0.0.0-20250215225251-76503f3f2cd8/go.mod h1:EeJ6hdGqHhrDJWlZBlNYto7U2yzxjzh1+F5Otyi/2O8= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= diff --git a/logger/level.go b/logger/level.go deleted file mode 100644 index 175995f..0000000 --- a/logger/level.go +++ /dev/null @@ -1,27 +0,0 @@ -package logger - -type LogLevel int - -const ( - DEBUG LogLevel = iota - INFO - WARN - ERROR - FATAL -) - -var levelStrings = map[LogLevel]string{ - DEBUG: "DEBUG", - INFO: "INFO", - WARN: "WARN", - ERROR: "ERROR", - FATAL: "FATAL", -} - -// String returns the string representation of the log level -func (l LogLevel) String() string { - if s, ok := levelStrings[l]; ok { - return s - } - return "UNKNOWN" -} diff --git a/logger/logger.go b/logger/logger.go deleted file mode 100644 index 9ef486d..0000000 --- a/logger/logger.go +++ /dev/null @@ -1,106 +0,0 @@ -package logger - -import ( - "fmt" - "log" - "os" - "sync" - "time" -) - -// Logger struct holds the logger instance -type Logger struct { - logger *log.Logger - level LogLevel -} - -var ( - defaultLogger *Logger - once sync.Once -) - -// NewLogger creates a new logger instance -func NewLogger() *Logger { - return &Logger{ - logger: log.New(os.Stdout, "", 0), - level: DEBUG, - } -} - -// Init initializes the default logger -func Init() *Logger { - once.Do(func() { - defaultLogger = NewLogger() - }) - return defaultLogger -} - -// GetLogger returns the default logger instance -func GetLogger() *Logger { - if defaultLogger == nil { - Init() - } - return defaultLogger -} - -// SetLevel sets the minimum logging level -func (l *Logger) SetLevel(level LogLevel) { - l.level = level -} - -// log handles the actual logging -func (l *Logger) log(level LogLevel, format string, args ...interface{}) { - if level < l.level { - return - } - timestamp := time.Now().Format("2006/01/02 15:04:05") - message := fmt.Sprintf(format, args...) - l.logger.Printf("%s: %s %s", level.String(), timestamp, message) -} - -// Debug logs debug level messages -func (l *Logger) Debug(format string, args ...interface{}) { - l.log(DEBUG, format, args...) -} - -// Info logs info level messages -func (l *Logger) Info(format string, args ...interface{}) { - l.log(INFO, format, args...) -} - -// Warn logs warning level messages -func (l *Logger) Warn(format string, args ...interface{}) { - l.log(WARN, format, args...) -} - -// Error logs error level messages -func (l *Logger) Error(format string, args ...interface{}) { - l.log(ERROR, format, args...) -} - -// Fatal logs fatal level messages and exits -func (l *Logger) Fatal(format string, args ...interface{}) { - l.log(FATAL, format, args...) - os.Exit(1) -} - -// Global helper functions -func Debug(format string, args ...interface{}) { - GetLogger().Debug(format, args...) -} - -func Info(format string, args ...interface{}) { - GetLogger().Info(format, args...) -} - -func Warn(format string, args ...interface{}) { - GetLogger().Warn(format, args...) -} - -func Error(format string, args ...interface{}) { - GetLogger().Error(format, args...) -} - -func Fatal(format string, args ...interface{}) { - GetLogger().Fatal(format, args...) -} diff --git a/main.go b/main.go index d684038..0435f65 100644 --- a/main.go +++ b/main.go @@ -15,8 +15,8 @@ import ( "syscall" "time" - "github.com/fosrl/client/logger" - "github.com/fosrl/client/websocket" + "github.com/fosrl/newt/logger" + "github.com/fosrl/newt/websocket" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" diff --git a/websocket/client.go b/websocket/client.go deleted file mode 100644 index 14d19e1..0000000 --- a/websocket/client.go +++ /dev/null @@ -1,351 +0,0 @@ -package websocket - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - "sync" - "time" - - "github.com/fosrl/client/logger" - - "github.com/gorilla/websocket" -) - -type Client struct { - conn *websocket.Conn - config *Config - baseURL string - handlers map[string]MessageHandler - done chan struct{} - handlersMux sync.RWMutex - - reconnectInterval time.Duration - isConnected bool - reconnectMux sync.RWMutex - - onConnect func() error -} - -type ClientOption func(*Client) - -type MessageHandler func(message WSMessage) - -// WithBaseURL sets the base URL for the client -func WithBaseURL(url string) ClientOption { - return func(c *Client) { - c.baseURL = url - } -} - -func (c *Client) OnConnect(callback func() error) { - c.onConnect = callback -} - -// NewClient creates a new Client client -func NewClient(clientID, secret string, endpoint string, opts ...ClientOption) (*Client, error) { - config := &Config{ - ClientID: clientID, - Secret: secret, - Endpoint: endpoint, - } - - client := &Client{ - config: config, - baseURL: endpoint, // default value - handlers: make(map[string]MessageHandler), - done: make(chan struct{}), - reconnectInterval: 10 * time.Second, - isConnected: false, - } - - // Apply options before loading config - for _, opt := range opts { - opt(client) - } - - // Load existing config if available - if err := client.loadConfig(); err != nil { - return nil, fmt.Errorf("failed to load config: %w", err) - } - - return client, nil -} - -// Connect establishes the WebSocket connection -func (c *Client) Connect() error { - go c.connectWithRetry() - return nil -} - -// Close closes the WebSocket connection -func (c *Client) Close() error { - close(c.done) - if c.conn != nil { - return c.conn.Close() - } - - // stop the ping monitor - c.setConnected(false) - - return nil -} - -// SendMessage sends a message through the WebSocket connection -func (c *Client) SendMessage(messageType string, data interface{}) error { - if c.conn == nil { - return fmt.Errorf("not connected") - } - - msg := WSMessage{ - Type: messageType, - Data: data, - } - - return c.conn.WriteJSON(msg) -} - -// RegisterHandler registers a handler for a specific message type -func (c *Client) RegisterHandler(messageType string, handler MessageHandler) { - c.handlersMux.Lock() - defer c.handlersMux.Unlock() - c.handlers[messageType] = handler -} - -// readPump pumps messages from the WebSocket connection -func (c *Client) readPump() { - defer c.conn.Close() - - for { - select { - case <-c.done: - return - default: - var msg WSMessage - err := c.conn.ReadJSON(&msg) - if err != nil { - return - } - - c.handlersMux.RLock() - if handler, ok := c.handlers[msg.Type]; ok { - handler(msg) - } - c.handlersMux.RUnlock() - } - } -} - -func (c *Client) getToken() (string, error) { - // Parse the base URL to ensure we have the correct hostname - baseURL, err := url.Parse(c.baseURL) - if err != nil { - return "", fmt.Errorf("failed to parse base URL: %w", err) - } - - // Ensure we have the base URL without trailing slashes - baseEndpoint := strings.TrimRight(baseURL.String(), "/") - - // If we already have a token, try to use it - if c.config.Token != "" { - tokenCheckData := map[string]interface{}{ - "clientId": c.config.ClientID, - "secret": c.config.Secret, - "token": c.config.Token, - } - jsonData, err := json.Marshal(tokenCheckData) - if err != nil { - return "", fmt.Errorf("failed to marshal token check data: %w", err) - } - - // Create a new request - req, err := http.NewRequest( - "POST", - baseEndpoint+"/api/v1/auth/client/get-token", - bytes.NewBuffer(jsonData), - ) - if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) - } - - // Set headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-CSRF-Token", "x-csrf-protection") - - // Make the request - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("failed to check token validity: %w", err) - } - defer resp.Body.Close() - - var tokenResp TokenResponse - if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { - return "", fmt.Errorf("failed to decode token check response: %w", err) - } - - // If token is still valid, return it - if tokenResp.Success && tokenResp.Message == "Token session already valid" { - return c.config.Token, nil - } - } - - // Get a new token - tokenData := map[string]interface{}{ - "clientId": c.config.ClientID, - "secret": c.config.Secret, - } - jsonData, err := json.Marshal(tokenData) - if err != nil { - return "", fmt.Errorf("failed to marshal token request data: %w", err) - } - - // Create a new request - req, err := http.NewRequest( - "POST", - baseEndpoint+"/api/v1/auth/client/get-token", - bytes.NewBuffer(jsonData), - ) - if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) - } - - // Set headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-CSRF-Token", "x-csrf-protection") - - // Make the request - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("failed to request new token: %w", err) - } - defer resp.Body.Close() - - var tokenResp TokenResponse - if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { - return "", fmt.Errorf("failed to decode token response: %w", err) - } - - if !tokenResp.Success { - return "", fmt.Errorf("failed to get token: %s", tokenResp.Message) - } - - if tokenResp.Data.Token == "" { - return "", fmt.Errorf("received empty token from server") - } - - return tokenResp.Data.Token, nil -} - -func (c *Client) connectWithRetry() { - for { - select { - case <-c.done: - return - default: - err := c.establishConnection() - if err != nil { - logger.Error("Failed to connect: %v. Retrying in %v...", err, c.reconnectInterval) - time.Sleep(c.reconnectInterval) - continue - } - return - } - } -} - -func (c *Client) establishConnection() error { - // Get token for authentication - token, err := c.getToken() - if err != nil { - return fmt.Errorf("failed to get token: %w", err) - } - - // Parse the base URL to determine protocol and hostname - baseURL, err := url.Parse(c.baseURL) - if err != nil { - return fmt.Errorf("failed to parse base URL: %w", err) - } - - // Determine WebSocket protocol based on HTTP protocol - wsProtocol := "wss" - if baseURL.Scheme == "http" { - wsProtocol = "ws" - } - - // Create WebSocket URL - wsURL := fmt.Sprintf("%s://%s/api/v1/ws", wsProtocol, baseURL.Host) - u, err := url.Parse(wsURL) - if err != nil { - return fmt.Errorf("failed to parse WebSocket URL: %w", err) - } - - // Add token to query parameters - q := u.Query() - q.Set("token", token) - u.RawQuery = q.Encode() - - // Connect to WebSocket - conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) - if err != nil { - return fmt.Errorf("failed to connect to WebSocket: %w", err) - } - - c.conn = conn - c.setConnected(true) - - // Start the ping monitor - go c.pingMonitor() - // Start the read pump - go c.readPump() - - if c.onConnect != nil { - err := c.saveConfig() - if err != nil { - logger.Error("Failed to save config: %v", err) - } - if err := c.onConnect(); err != nil { - logger.Error("OnConnect callback failed: %v", err) - } - } - - return nil -} - -func (c *Client) pingMonitor() { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - for { - select { - case <-c.done: - return - case <-ticker.C: - if err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil { - logger.Error("Ping failed: %v", err) - c.reconnect() - return - } - } - } -} - -func (c *Client) reconnect() { - c.setConnected(false) - if c.conn != nil { - c.conn.Close() - } - - go c.connectWithRetry() -} - -func (c *Client) setConnected(status bool) { - c.reconnectMux.Lock() - defer c.reconnectMux.Unlock() - c.isConnected = status -} diff --git a/websocket/config.go b/websocket/config.go deleted file mode 100644 index bc51204..0000000 --- a/websocket/config.go +++ /dev/null @@ -1,72 +0,0 @@ -package websocket - -import ( - "encoding/json" - "log" - "os" - "path/filepath" - "runtime" -) - -func getConfigPath() string { - var configDir string - switch runtime.GOOS { - case "darwin": - configDir = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "client-client") - case "windows": - configDir = filepath.Join(os.Getenv("APPDATA"), "client-client") - default: // linux and others - configDir = filepath.Join(os.Getenv("HOME"), ".config", "client-client") - } - - if err := os.MkdirAll(configDir, 0755); err != nil { - log.Printf("Failed to create config directory: %v", err) - } - - return filepath.Join(configDir, "config.json") -} - -func (c *Client) loadConfig() error { - if c.config.ClientID != "" && c.config.Secret != "" && c.config.Endpoint != "" { - return nil - } - - configPath := getConfigPath() - data, err := os.ReadFile(configPath) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return err - } - - var config Config - if err := json.Unmarshal(data, &config); err != nil { - return err - } - - if c.config.ClientID == "" { - c.config.ClientID = config.ClientID - } - if c.config.Token == "" { - c.config.Token = config.Token - } - if c.config.Secret == "" { - c.config.Secret = config.Secret - } - if c.config.Endpoint == "" { - c.config.Endpoint = config.Endpoint - c.baseURL = config.Endpoint - } - - return nil -} - -func (c *Client) saveConfig() error { - configPath := getConfigPath() - data, err := json.MarshalIndent(c.config, "", " ") - if err != nil { - return err - } - return os.WriteFile(configPath, data, 0644) -} diff --git a/websocket/types.go b/websocket/types.go deleted file mode 100644 index 9a72e90..0000000 --- a/websocket/types.go +++ /dev/null @@ -1,21 +0,0 @@ -package websocket - -type Config struct { - ClientID string `json:"clientId"` - Secret string `json:"secret"` - Token string `json:"token"` - Endpoint string `json:"endpoint"` -} - -type TokenResponse struct { - Data struct { - Token string `json:"token"` - } `json:"data"` - Success bool `json:"success"` - Message string `json:"message"` -} - -type WSMessage struct { - Type string `json:"type"` - Data interface{} `json:"data"` -}