diff --git a/README.md b/README.md index d9fc8a0..d36039d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ It supports authenticated publishing, subscribing, rate-limited usage tracking, - [x] Forward messages to webhook endpoints (POST method) with flexible JSON template formatting - [x] Health monitoring and system metrics - [x] Debug mode with detailed timing and proxy information +- [x] Development mode with `--disable-auth` flag for testing --- @@ -46,6 +47,14 @@ Download from [releases](https://github.com/getoptimum/mump2p-cli/releases/lates ./mump2p whoami # Check your session ``` +**Development/Testing Mode:** +```sh +# Skip authentication for testing (requires --client-id and --service-url) +./mump2p --disable-auth --client-id="my-test-client" publish --topic=test --message="Hello" --service-url="http://34.146.222.111:8080" +./mump2p --disable-auth --client-id="my-test-client" subscribe --topic=test --service-url="http://34.146.222.111:8080" +./mump2p --disable-auth --client-id="my-test-client" list-topics --service-url="http://34.146.222.111:8080" +``` + ### 3. Basic Usage ```sh @@ -243,7 +252,20 @@ Error: required flag(s) "topic" not set - Include all required arguments - Check flag spelling and syntax -### **6. Debug Mode & Performance Analysis** +### **6. Development Mode (`--disable-auth`)** + +For development and testing, you can bypass authentication: + +```sh +# Requires --client-id and --service-url flags +./mump2p --disable-auth --client-id="test-client" \ + publish --topic=test --message="Hello" \ + --service-url="http://34.146.222.111:8080" +``` + +> **Note:** This mode is for testing only. No rate limits enforced. See [guide](./docs/guide.md) for full details. + +### **7. Debug Mode & Performance Analysis** The `--debug` flag provides detailed timing and proxy information for troubleshooting: @@ -251,6 +273,11 @@ The `--debug` flag provides detailed timing and proxy information for troublesho # Enable debug mode for operations ./mump2p --debug publish --topic=test-topic --message='Hello World' ./mump2p --debug subscribe --topic=test-topic + +# Combine with --disable-auth for testing +./mump2p --disable-auth --client-id="test" --debug \ + publish --topic=test --message="Hello" \ + --service-url="http://34.146.222.111:8080" ``` For comprehensive debug mode usage, performance analysis, and blast testing examples, see the [Complete User Guide](./docs/guide.md#debug-mode). diff --git a/cmd/auth.go b/cmd/auth.go index 10a0c52..8f79631 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -57,6 +57,21 @@ var whoamiCmd = &cobra.Command{ Short: "Show current authentication status", Long: `Display information about the current authentication token.`, RunE: func(cmd *cobra.Command, args []string) error { + if IsAuthDisabled() { + // Display authentication status when auth is disabled + clientIDToUse := GetClientID() + if clientIDToUse == "" { + clientIDToUse = "(not set - use --client-id flag)" + } + fmt.Println("Authentication Status:") + fmt.Println("----------------------") + fmt.Printf("Client ID: %s\n", clientIDToUse) + fmt.Println("Auth Mode: Disabled (using --disable-auth)") + fmt.Println("Rate Limits: N/A (no limits enforced)") + fmt.Println("Token: N/A (auth disabled)") + return nil + } + // load token storage := auth.NewStorageWithPath(GetAuthPath()) token, err := storage.LoadToken() @@ -105,6 +120,11 @@ var refreshCmd = &cobra.Command{ Short: "Refresh the authentication token", Long: `Manually refresh the authentication token before it expires.`, RunE: func(cmd *cobra.Command, args []string) error { + if IsAuthDisabled() { + fmt.Println("Token refresh skipped (auth disabled)") + return nil + } + // create auth client and storage authClient := auth.NewClient() storage := auth.NewStorageWithPath(GetAuthPath()) diff --git a/cmd/list.go b/cmd/list.go index 63ff56b..bc1e6dc 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -28,6 +28,72 @@ var listTopicsCmd = &cobra.Command{ Long: `List all topics that the authenticated client is currently subscribed to. This command shows your active topics and their count.`, RunE: func(cmd *cobra.Command, args []string) error { + if IsAuthDisabled() { + // When auth is disabled, require client-id flag + clientIDToUse := GetClientID() + if clientIDToUse == "" { + return fmt.Errorf("--client-id is required when using --disable-auth") + } + + // Determine service URL + serviceURL := config.LoadConfig().ServiceUrl + if listServiceURL != "" { + serviceURL = listServiceURL + fmt.Printf("Using custom service URL: %s\n", serviceURL) + } + + // Create HTTP GET request to /api/v1/topics with client_id query parameter + endpoint := fmt.Sprintf("%s/api/v1/topics?client_id=%s", serviceURL, clientIDToUse) + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %v", err) + } + + // Set headers (no auth needed for disabled auth) + req.Header.Set("Content-Type", "application/json") + + // Execute the request + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("HTTP GET request failed: %v", err) + } + defer resp.Body.Close() + + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %v", err) + } + + // Check for HTTP errors + if resp.StatusCode != 200 { + return fmt.Errorf("HTTP GET request error (status %d): %s", resp.StatusCode, string(body)) + } + + // Parse the JSON response + var listResponse ListResponse + if err := json.Unmarshal(body, &listResponse); err != nil { + return fmt.Errorf("failed to parse response JSON: %v", err) + } + + // Display results in a formatted table + fmt.Printf("\nšŸ“‹ Subscribed Topics for Client: %s (Auth Disabled)\n", listResponse.ClientID) + fmt.Printf("═══════════════════════════════════════════════════════════════\n") + + if listResponse.Count == 0 { + fmt.Printf(" No active topics found.\n") + fmt.Printf(" Use './mump2p subscribe --topic=' to subscribe to a topic.\n") + } else { + fmt.Printf(" Total Topics: %d\n\n", listResponse.Count) + for i, topic := range listResponse.Topics { + fmt.Printf(" %d. %s\n", i+1, topic) + } + } + + fmt.Printf("═══════════════════════════════════════════════════════════════\n") + return nil + } + // Authenticate authClient := auth.NewClient() storage := auth.NewStorageWithPath(GetAuthPath()) diff --git a/cmd/publish.go b/cmd/publish.go index 4343065..6b860bc 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -69,21 +69,35 @@ var publishCmd = &cobra.Command{ return errors.New("only one of --message or --file should be used at a time") } - authClient := auth.NewClient() - storage := auth.NewStorageWithPath(GetAuthPath()) - token, err := authClient.GetValidToken(storage) - if err != nil { - return fmt.Errorf("authentication required: %v", err) - } - // parse token to check if the account is active - parser := auth.NewTokenParser() - claims, err := parser.ParseToken(token.Token) - if err != nil { - return fmt.Errorf("error parsing token: %v", err) - } - // check if the account is active - if !claims.IsActive { - return fmt.Errorf("your account is inactive, please contact support") + var claims *auth.TokenClaims + var token *auth.StoredToken + var clientIDToUse string + + if !IsAuthDisabled() { + authClient := auth.NewClient() + storage := auth.NewStorageWithPath(GetAuthPath()) + var err error + token, err = authClient.GetValidToken(storage) + if err != nil { + return fmt.Errorf("authentication required: %v", err) + } + // parse token to check if the account is active + parser := auth.NewTokenParser() + claims, err = parser.ParseToken(token.Token) + if err != nil { + return fmt.Errorf("error parsing token: %v", err) + } + // check if the account is active + if !claims.IsActive { + return fmt.Errorf("your account is inactive, please contact support") + } + clientIDToUse = claims.ClientID + } else { + // When auth is disabled, require client-id flag + clientIDToUse = GetClientID() + if clientIDToUse == "" { + return fmt.Errorf("--client-id is required when using --disable-auth") + } } var ( data []byte @@ -104,14 +118,17 @@ var publishCmd = &cobra.Command{ // message size messageSize := int64(len(data)) - limiter, err := ratelimit.NewRateLimiterWithDir(claims, GetAuthDir()) - if err != nil { - return fmt.Errorf("rate limiter setup failed: %v", err) - } + // Skip rate limiting if auth is disabled + if !IsAuthDisabled() { + limiter, err := ratelimit.NewRateLimiterWithDir(claims, GetAuthDir()) + if err != nil { + return fmt.Errorf("rate limiter setup failed: %v", err) + } - // check all rate limits: size, quota, per-hr, per-sec - if err := limiter.CheckPublishAllowed(messageSize); err != nil { - return err + // check all rate limits: size, quota, per-hr, per-sec + if err := limiter.CheckPublishAllowed(messageSize); err != nil { + return err + } } // use custom service URL if provided, otherwise use the default @@ -152,7 +169,7 @@ var publishCmd = &cobra.Command{ } defer client.Close() - err = client.Publish(ctx, claims.ClientID, pubTopic, publishData) + err = client.Publish(ctx, clientIDToUse, pubTopic, publishData) if err != nil { return fmt.Errorf("gRPC publish failed: %v", err) } @@ -178,7 +195,7 @@ var publishCmd = &cobra.Command{ } reqData := PublishRequest{ - ClientID: claims.ClientID, + ClientID: clientIDToUse, Topic: pubTopic, Message: string(publishData), // use modified data with debug prefix if enabled Timestamp: time.Now().UnixMilli(), @@ -193,7 +210,10 @@ var publishCmd = &cobra.Command{ if err != nil { return err } - req.Header.Set("Authorization", "Bearer "+token.Token) + // Only set Authorization header if auth is enabled + if !IsAuthDisabled() && token != nil { + req.Header.Set("Authorization", "Bearer "+token.Token) + } req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) @@ -215,8 +235,11 @@ var publishCmd = &cobra.Command{ fmt.Println(string(body)) } - if limiter, err := ratelimit.NewRateLimiterWithDir(claims, GetAuthDir()); err == nil { - _ = limiter.RecordPublish(messageSize) // update quota (fail silently) + // Only record publish if auth is enabled + if !IsAuthDisabled() { + if limiter, err := ratelimit.NewRateLimiterWithDir(claims, GetAuthDir()); err == nil { + _ = limiter.RecordPublish(messageSize) // update quota (fail silently) + } } return nil }, diff --git a/cmd/root.go b/cmd/root.go index 9bba281..c767937 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "os" "path/filepath" @@ -9,8 +8,10 @@ import ( ) var ( - authPath string // Global flag for custom authentication file path - debug bool // Global flag for debug mode + authPath string // Global flag for custom authentication file path + debug bool // Global flag for debug mode + disableAuth bool // Global flag to disable authentication checks + clientID string // Global flag for client ID (used when auth is disabled) ) var rootCmd = &cobra.Command{ @@ -22,7 +23,6 @@ without relying on the HTTP server. It directly invokes Go services.`, func Execute() { if err := rootCmd.Execute(); err != nil { - fmt.Println("Error:", err) os.Exit(1) } } @@ -34,6 +34,12 @@ func init() { // Add global debug flag rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug mode with detailed timing and proxy information") + // Add global disable auth flag + rootCmd.PersistentFlags().BoolVar(&disableAuth, "disable-auth", false, "Disable authentication checks (for testing/development)") + + // Add global client ID flag + rootCmd.PersistentFlags().StringVar(&clientID, "client-id", "", "Client ID to use (required when --disable-auth is enabled)") + // disable completion option rootCmd.CompletionOptions.DisableDefaultCmd = true } @@ -56,3 +62,13 @@ func GetAuthDir() string { func IsDebugMode() bool { return debug } + +// IsAuthDisabled returns true if authentication is disabled +func IsAuthDisabled() bool { + return disableAuth +} + +// GetClientID returns the client ID when auth is disabled +func GetClientID() string { + return clientID +} diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 572cad6..70a6e38 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -66,22 +66,36 @@ var subscribeCmd = &cobra.Command{ Use: "subscribe", Short: "Subscribe to a topic via WebSocket or gRPC stream", RunE: func(cmd *cobra.Command, args []string) error { - // auth - authClient := auth.NewClient() - storage := auth.NewStorageWithPath(GetAuthPath()) - token, err := authClient.GetValidToken(storage) - if err != nil { - return fmt.Errorf("authentication required: %v", err) - } - // parse token to check if the account is active - parser := auth.NewTokenParser() - claims, err := parser.ParseToken(token.Token) - if err != nil { - return fmt.Errorf("error parsing token: %v", err) - } - // check if the account is active - if !claims.IsActive { - return fmt.Errorf("your account is inactive, please contact support") + var claims *auth.TokenClaims + var token *auth.StoredToken + var clientIDToUse string + + if !IsAuthDisabled() { + // auth + authClient := auth.NewClient() + storage := auth.NewStorageWithPath(GetAuthPath()) + var err error + token, err = authClient.GetValidToken(storage) + if err != nil { + return fmt.Errorf("authentication required: %v", err) + } + // parse token to check if the account is active + parser := auth.NewTokenParser() + claims, err = parser.ParseToken(token.Token) + if err != nil { + return fmt.Errorf("error parsing token: %v", err) + } + // check if the account is active + if !claims.IsActive { + return fmt.Errorf("your account is inactive, please contact support") + } + clientIDToUse = claims.ClientID + } else { + // When auth is disabled, require client-id flag + clientIDToUse = GetClientID() + if clientIDToUse == "" { + return fmt.Errorf("--client-id is required when using --disable-auth") + } } // setup persistence if path is provided @@ -171,7 +185,7 @@ var subscribeCmd = &cobra.Command{ } defer client.Close() - err = client.SubscribeTopic(ctx, claims.ClientID, subTopic, subThreshold) + err = client.SubscribeTopic(ctx, clientIDToUse, subTopic, subThreshold) if err != nil { return fmt.Errorf("gRPC subscribe failed: %v", err) } @@ -182,7 +196,7 @@ var subscribeCmd = &cobra.Command{ fmt.Println("Sending HTTP POST subscription request...") httpEndpoint := fmt.Sprintf("%s/api/v1/subscribe", srcUrl) reqData := SubscribeRequest{ - ClientID: claims.ClientID, + ClientID: clientIDToUse, Topic: subTopic, Threshold: subThreshold, } @@ -195,7 +209,10 @@ var subscribeCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to create HTTP request: %v", err) } - req.Header.Set("Authorization", "Bearer "+token.Token) + // Only set Authorization header if auth is enabled + if !IsAuthDisabled() && token != nil { + req.Header.Set("Authorization", "Bearer "+token.Token) + } req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) @@ -232,7 +249,7 @@ var subscribeCmd = &cobra.Command{ } defer streamClient.Close() - msgChan, err := streamClient.Subscribe(streamCtx, claims.ClientID, grpcBufferSize) + msgChan, err := streamClient.Subscribe(streamCtx, clientIDToUse, grpcBufferSize) if err != nil { return fmt.Errorf("gRPC stream subscribe failed: %v", err) } @@ -251,7 +268,7 @@ var subscribeCmd = &cobra.Command{ defer cancel() // Format the payload using template - formattedPayload, err := webhookFormatter.FormatMessage(payload, subTopic, claims.ClientID, "grpc-msg") + formattedPayload, err := webhookFormatter.FormatMessage(payload, subTopic, clientIDToUse, "grpc-msg") if err != nil { fmt.Printf("Failed to format webhook payload: %v\n", err) return @@ -326,7 +343,7 @@ var subscribeCmd = &cobra.Command{ // convert HTTP URL to WebSocket URL wsURL := strings.Replace(srcUrl, "http://", "ws://", 1) wsURL = strings.Replace(wsURL, "https://", "wss://", 1) - wsURL = fmt.Sprintf("%s/api/v1/ws?client_id=%s", wsURL, claims.ClientID) + wsURL = fmt.Sprintf("%s/api/v1/ws?client_id=%s", wsURL, clientIDToUse) // Extract receiver IP for debug mode receiverAddr := extractIPFromURL(srcUrl) @@ -336,7 +353,10 @@ var subscribeCmd = &cobra.Command{ // setup ws headers for authentication header := http.Header{} - header.Set("Authorization", "Bearer "+token.Token) + // Only set Authorization header if auth is enabled + if !IsAuthDisabled() && token != nil { + header.Set("Authorization", "Bearer "+token.Token) + } // connect conn, _, err := websocket.DefaultDialer.Dial(wsURL, header) @@ -359,7 +379,7 @@ var subscribeCmd = &cobra.Command{ defer cancel() // Format the payload using template - formattedPayload, err := webhookFormatter.FormatMessage(payload, subTopic, claims.ClientID, "ws-msg") + formattedPayload, err := webhookFormatter.FormatMessage(payload, subTopic, clientIDToUse, "ws-msg") if err != nil { fmt.Printf("Failed to format webhook payload: %v\n", err) return diff --git a/cmd/usages.go b/cmd/usages.go index 05074b2..048c203 100644 --- a/cmd/usages.go +++ b/cmd/usages.go @@ -14,6 +14,14 @@ var usageCmd = &cobra.Command{ Use: "usage", Short: "Display usage statistics and rate limits", RunE: func(cmd *cobra.Command, args []string) error { + if IsAuthDisabled() { + // When auth is disabled, usage tracking is not available + fmt.Println("Usage Statistics:") + fmt.Println(" Status: Usage tracking disabled (using --disable-auth)") + fmt.Println(" No rate limits or quotas are enforced in this mode") + return nil + } + // get valid token (refreshes if needed) authClient := auth.NewClient() storage := auth.NewStorageWithPath(GetAuthPath()) diff --git a/docs/guide.md b/docs/guide.md index df66a67..5c7a955 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -7,6 +7,7 @@ After completing the README's quick start, this guide will teach you: - **Authentication Management**: Token management, refresh, and troubleshooting +- **Development Mode**: Testing without authentication using `--disable-auth` flag - **Service Configuration**: Using different proxy servers and custom URLs - **Protocol Deep Dive**: When to use HTTP/WebSocket vs gRPC - **Advanced Features**: Message persistence, webhooks, and monitoring @@ -100,6 +101,34 @@ export MUMP2P_AUTH_PATH="/opt/mump2p/auth/token.yml" - Rate limiting usage files will be stored in the same directory - Ensure the user has write permissions to the specified directory +### Development/Testing Mode + +For development and testing scenarios, you can bypass authentication entirely using the `--disable-auth` flag: + +```sh +# All commands work without login (requires --client-id and --service-url) +./mump2p --disable-auth --client-id="my-test-client" whoami +./mump2p --disable-auth --client-id="my-test-client" publish --topic=test --message="Hello" --service-url="http://34.146.222.111:8080" +./mump2p --disable-auth --client-id="my-test-client" subscribe --topic=test --service-url="http://34.146.222.111:8080" +./mump2p --disable-auth --client-id="my-test-client" list-topics --service-url="http://34.146.222.111:8080" +./mump2p --disable-auth usage + +# Works with gRPC too +./mump2p --disable-auth --client-id="my-test-client" --grpc publish --topic=test --message="Hello" --service-url="http://34.146.222.111:8080" +./mump2p --disable-auth --client-id="my-test-client" --grpc subscribe --topic=test --service-url="http://34.146.222.111:8080" + +# Combine with debug mode +./mump2p --disable-auth --client-id="my-test-client" --debug publish --topic=test --message="Hello" --service-url="http://34.146.222.111:8080" +``` + +**When using `--disable-auth`:** +- **Must provide `--client-id` flag** with your chosen client ID +- No rate limits enforced (bypasses all quotas) +- No usage tracking +- All functionality works without authentication +- **Requires `--service-url` for network operations** (publish, subscribe, list-topics) +- Works with both HTTP/WebSocket and gRPC protocols + ### Logout To remove your stored authentication token: