diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47c4f7b..cffbe27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,17 +54,3 @@ jobs: # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true working-directory: ./ - - fuzz: - name: fuzz tests - runs-on: ubuntu-latest - steps: - - uses: actions/setup-go@v5 - with: - go-version: ${{ env.go-version }} - cache: true - - uses: actions/checkout@v4 - - name: Build CLI binary - run: make build - - name: Run fuzz tests - run: make e2e-fuzz diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55b87e5..f595313 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ on: jobs: build-and-test: - name: Build and E2E Test + name: Build and test runs-on: ubuntu-latest env: DOMAIN: ${{ secrets.AUTH0_DOMAIN }} @@ -42,14 +42,6 @@ jobs: AUDIENCE="${AUDIENCE}" \ SERVICE_URL="${SERVICE_URL}" - - name: Run E2E Tests - env: - MUMP2P_E2E_TOKEN_B64: ${{ secrets.MUMP2P_E2E_TOKEN_B64 }} - run: make e2e-test - - - name: Run Fuzz Tests - run: make e2e-fuzz - - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: @@ -81,12 +73,5 @@ jobs: - name: Auth GH CLI using PAT run: echo "${{ secrets.RELEASE_GH_TOKEN }}" | gh auth login --with-token - - name: Set Release Variables - id: vars - run: | - echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - echo "CLI_NAME=$(make -s print-cli-name)" >> $GITHUB_ENV - - name: Create Release - run: | - make release \ No newline at end of file + run: make release \ No newline at end of file diff --git a/Makefile b/Makefile index e0adfe2..b5dadb6 100644 --- a/Makefile +++ b/Makefile @@ -2,44 +2,39 @@ GO_BIN ?= go CLI_NAME := mump2p BUILD_DIR := dist -VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") -COMMIT_HASH ?= $(shell git rev-parse --short HEAD) DOMAIN ?= "" CLIENT_ID ?= "" AUDIENCE ?= optimum-login -SERVICE_URL ?= http://localhost:12080 +SERVICE_URL ?= http://us1-proxy.getoptimum.io:8080 LD_FLAGS := -X github.com/getoptimum/mump2p-cli/internal/config.Domain=$(DOMAIN) \ -X github.com/getoptimum/mump2p-cli/internal/config.ClientID=$(CLIENT_ID) \ -X github.com/getoptimum/mump2p-cli/internal/config.Audience=$(AUDIENCE) \ - -X github.com/getoptimum/mump2p-cli/internal/config.ServiceURL=$(SERVICE_URL) \ - -X github.com/getoptimum/mump2p-cli/internal/config.Version=$(VERSION) \ - -X github.com/getoptimum/mump2p-cli/internal/config.CommitHash=$(COMMIT_HASH) + -X github.com/getoptimum/mump2p-cli/internal/config.ServiceURL=$(SERVICE_URL) -.DEFAULT_GOAL := help - -.PHONY: all build run clean test help lint build tag release print-cli-name e2e-test e2e-fuzz coverage +.PHONY: all build run clean test lint tag release print-cli-name coverage all: lint build -lint: ## Run linter +lint: golangci-lint run ./... -build: ## Build the CLI binary +build: GOOS=darwin GOARCH=amd64 $(GO_BIN) build -ldflags="$(LD_FLAGS)" -o $(BUILD_DIR)/$(CLI_NAME)-mac . GOOS=linux GOARCH=amd64 $(GO_BIN) build -ldflags="$(LD_FLAGS)" -o $(BUILD_DIR)/$(CLI_NAME)-linux . -print-cli-name: ## Print CLI name for CI/CD usage +print-cli-name: @echo "$(CLI_NAME)" -release: build ## Build and create GitHub release - @echo "Creating release for $(VERSION)" - mkdir -p $(BUILD_DIR) - gh release create $(VERSION) \ - --title "Release $(VERSION)" \ - --notes "Release notes for $(VERSION)" \ +release: build + @tag=$$(git describe --tags --abbrev=0 2>/dev/null || echo v0.0.0); \ + echo "Creating release for $$tag"; \ + mkdir -p $(BUILD_DIR); \ + gh release create "$$tag" \ + --title "Release $$tag" \ + --notes "Release notes for $$tag" \ $(BUILD_DIR)/$(CLI_NAME)-mac \ - $(BUILD_DIR)/$(CLI_NAME)-linux \ + $(BUILD_DIR)/$(CLI_NAME)-linux tag: @echo "Calculating next RC tag..." @@ -56,46 +51,17 @@ tag: git tag -a $$new_tag -m "Release $$new_tag"; \ git push origin $$new_tag -run: build ## Run the CLI with default config +run: build ./$(CLI_NAME) --config=$(CONFIG_PATH) -clean: ## Clean up build artifacts +clean: rm -rf $(BUILD_DIR) rm -f coverage.out coverage.html -test: ## Run unit tests - $(GO_BIN) test $(shell $(GO_BIN) list ./... | grep -v /e2e) -v -count=1 +test: + $(GO_BIN) test ./... -v -count=1 -coverage: ## Run tests with coverage and generate HTML report - $(GO_BIN) test $(shell $(GO_BIN) list ./... | grep -v /e2e) -coverprofile=coverage.out -v -count=1 +coverage: + $(GO_BIN) test ./... -coverprofile=coverage.out -v -count=1 $(GO_BIN) tool cover -html=coverage.out -o coverage.html @echo "Coverage report generated: coverage.html" - -e2e-test: ## Run E2E tests against dist/ binary - @echo "Running E2E tests..." - @if [ ! -f "$(BUILD_DIR)/$(CLI_NAME)-linux" ] && [ ! -f "$(BUILD_DIR)/$(CLI_NAME)-mac" ]; then \ - echo "Error: No binary found in $(BUILD_DIR)/"; \ - echo "Run 'make build' first with release credentials"; \ - exit 1; \ - fi - go test ./e2e -v -timeout 10m - -e2e-fuzz: ## Run fuzz tests against dist/ binary - @echo "Running fuzz tests..." - @if [ ! -f "$(BUILD_DIR)/$(CLI_NAME)-linux" ] && [ ! -f "$(BUILD_DIR)/$(CLI_NAME)-mac" ]; then \ - echo "Error: No binary found in $(BUILD_DIR)/"; \ - echo "Run 'make build' first with release credentials"; \ - exit 1; \ - fi - @echo "Fuzzing publish topic names..." - @go test ./e2e -run='^$$' -fuzz=FuzzPublishTopicName -fuzztime=1m -timeout=3m - @echo "Fuzzing publish messages..." - @go test ./e2e -run='^$$' -fuzz=FuzzPublishMessage -fuzztime=1m -timeout=3m - @echo "Fuzzing service URLs..." - @go test ./e2e -run='^$$' -fuzz=FuzzServiceURL -fuzztime=1m -timeout=3m - @echo "Fuzzing file paths..." - @go test ./e2e -run='^$$' -fuzz=FuzzFilePath -fuzztime=1m -timeout=3m - @echo "All fuzz tests passed!" - -help: ## Show help - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' diff --git a/README.md b/README.md index ad35d87..42f3f3a 100644 --- a/README.md +++ b/README.md @@ -1,249 +1,271 @@ # mump2p CLI -`mump2p-cli` is the command-line interface for interacting with [Optimum network](https://github.com/getoptimum/optimum-p2p) — a high-performance RLNC-enhanced pubsub network. +CLI for interacting with the Optimum P2P network. Connects to a proxy for session management, then communicates directly with nodes for publish/subscribe. -It supports authenticated publishing, subscribing, rate-limited usage tracking, and JWT session management. +## Authentication ---- +### Login -## Features - -- [x] Publish messages to topics -- [x] Subscribe to real-time message streams -- [x] List active topics -- [x] gRPC support for high-performance streaming -- [x] JWT-based login/logout and token refresh -- [x] Local rate-limiting (publish count, quota, max size) -- [x] Usage statistics reporting -- [x] Persist messages to local storage -- [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 -- [x] Multiple output formats (table, JSON, YAML) for automation and scripting -- [x] Interactive tracer dashboard for real-time metrics visualization - ---- +```bash +mump2p login +``` -## Quick Start +``` +Initiating authentication... -### 1. Installation +To complete authentication: +1. Visit: https://dev-d4be5uc4a3c311t3.us.auth0.com/activate?user_code=XXXX-XXXX +2. Or go to https://dev-d4be5uc4a3c311t3.us.auth0.com/activate and enter code: XXXX-XXXX -**Quick Install (Recommended):** -The install script automatically detects your OS (Linux/macOS), downloads the latest release binary, makes it executable, and verifies the installation. +Waiting for you to complete authentication in the browser... -```sh -curl -sSL https://raw.githubusercontent.com/getoptimum/mump2p-cli/main/install.sh | bash +✅ Successfully authenticated +Token expires at: 07 Apr 26 23:51 IST ``` -**Expected Output:** -![CLI Installation Output](./docs/img/cli_command.png) - -**Manual Installation:** -Download from [releases](https://github.com/getoptimum/mump2p-cli/releases/latest) and make executable. - -### 2. Authentication +### Who Am I -```sh -./mump2p login -./mump2p whoami # Check your session +```bash +mump2p whoami ``` -**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" +``` +Authentication Status: +---------------------- +Client ID: auth0| +Expires: 07 Apr 26 23:51 IST +Valid for: 720h0m0s +Is Active: true + +Rate Limits: +------------ +Publish Rate: 50000 per hour +Publish Rate: 600 per second +Max Message Size: 10.00 MB +Daily Quota: 20480.00 MB ``` -### 3. Basic Usage +### Logout -```sh -# Subscribe to a topic (WebSocket) -./mump2p subscribe --topic=test-topic +```bash +mump2p logout +``` -# Subscribe via gRPC stream -./mump2p subscribe --topic=test-topic --grpc +``` +✅ Successfully logged out +``` -# Publish a message (HTTP) -./mump2p publish --topic=test-topic --message='Hello World' +## Subscribe -# Publish via gRPC -./mump2p publish --topic=test-topic --message='Hello World' --grpc +Subscribe to a topic and stream messages directly from a P2P node. By default, 3 nodes are requested for automatic failover. -# List your active topics -./mump2p list-topics +```bash +mump2p subscribe --topic test +``` -# Output formats - JSON/YAML for automation and scripting -./mump2p list-topics --output=json -./mump2p whoami --output=yaml +``` +Subscribed to 'test' on 34.126.161.115:33211 (Singapore) in 161ms — 2 backup nodes ready + backup: 34.141.111.130:33211 (Germany) + backup: 136.110.0.19:33211 (Singapore) +[test] Hello from authenticated CLI! +[test] Second authenticated message +^C +Disconnected — 2 messages in 14s +``` -# Debug mode - detailed timing and proxy information -./mump2p --debug publish --topic=test-topic --message='Hello World' -./mump2p --debug subscribe --topic=test-topic +### With single node -# Tracer dashboard - real-time metrics visualization -./mump2p tracer dashboard +```bash +mump2p subscribe --topic test --expose-amount 1 ``` -### Transport Protocols +``` +Subscribed to 'test' on 34.126.161.115:33211 (Singapore) in 175ms +[test] hello world +``` -- **HTTP/WebSocket (Default)**: Traditional REST API with WebSocket streaming -- **gRPC**: High-performance binary protocol with streaming support -- Use `--grpc` flag for both publishing and subscribing +### Failover ---- +If the first node is unreachable, the CLI automatically falls back to the next one: -## 📚 Documentation +``` + Node 136.110.0.19:33211 unreachable, falling back... +Subscribed to 'test' on 34.126.161.115:33211 (Singapore) in 312ms — 1 backup node ready + backup: 34.141.111.130:33211 (Germany) +``` + +### Persist messages to file -- **[Complete User Guide](./docs/guide.md)** - Detailed setup, authentication, and usage instructions +```bash +mump2p subscribe --topic test --persist ./messages.log +``` ---- +### Forward to webhook -## Version Compatibility +```bash +mump2p subscribe --topic test --webhook https://hooks.slack.com/services/xxx +mump2p subscribe --topic test --webhook https://discord.com/api/webhooks/xxx --webhook-schema '{"content":"{{.Message}}"}' +``` -**Important:** Always use the latest version binaries (currently **v0.0.1-rc8**) from the releases page. +## Publish -**Current Release:** -- **v0.0.1-rc8** is the latest release -- **v0.0.1-rc5** and earlier versions are deprecated +Publish a message to a topic directly to a P2P node. ---- +```bash +mump2p publish --topic test --message "Hello World" +``` -## FAQ - Common Issues & Troubleshooting +``` +Published to 34.126.161.115:33211 (Singapore) in 259ms +``` -### **1. Available Service URLs** +### From file -By default, the CLI uses the first proxy in the list below. You can override this using the `--service-url` flag or by rebuilding with a different `SERVICE_URL`. +```bash +mump2p publish --topic test/data --file ./payload.json +``` -| **Proxy Address** | **Location** | **URL** | **Notes** | -|---------------------|--------------|---------|-----------| -| `34.146.222.111` | Tokyo | `http://34.146.222.111:8080` | **Default** | -| `35.221.118.95` | Tokyo | `http://35.221.118.95:8080` | | -| `34.142.205.26` | Singapore | `http://34.142.205.26:8080` | | +## Debug Mode -> **Note:** More geo-locations coming soon! +Use `--debug` to see session details, node scores, timing breakdowns, message IDs, and peer paths. -**Example: Using a different proxy:** +```bash +mump2p subscribe --topic test --debug +``` -```sh -./mump2p-mac publish --topic=example-topic --message='Hello' --service-url="http://35.221.118.95:8080" -./mump2p-mac subscribe --topic=example-topic --service-url="http://34.142.205.26:8080" +``` +New session 09ddc264-e0bd-42af-b768-feacd512e686 from http://us1-proxy.getoptimum.io:8080 (535ms) | 3 node(s) available + Trying node 1/3: 34.126.161.115:33211 (Singapore, score: 0.98)... +Subscribed to 'test' on 34.126.161.115:33211 (Singapore) in 188ms — 2 backup nodes ready + backup: 34.141.111.130:33211 (Germany) + backup: 136.110.0.19:33211 (Singapore) +Recv: [1] receiver_addr:34.126.161.115 [recv_time, size]:[1775977506353069000, 21] topic:test hash:65456d67 protocol:gRPC-direct + from: 12D3KooWSviMsA9yDTxmv1svvGrEPeW6F5DYzSFQihE9YrQW6vtm + via: 34.126.161.115:33211 (Singapore) + id: 65456d676c97... ``` -### **2. Authentication & Account Issues** +```bash +mump2p publish --topic test --message "hello" --debug +``` -#### **Error: Connection refused** ``` -Error: HTTP publish failed: dial tcp [::1]:8080: connect: connection refused +New session 619ceb3b-9440-4654-923f-982bd6dba0be from http://us1-proxy.getoptimum.io:8080 (516ms) | 1 node(s) available + Trying node 1/1: 34.126.161.115:33211 (Singapore, score: 0.98)... +Session: 516ms | Published: 261ms | Total: 777ms +Published to 34.126.161.115:33211 (Singapore) in 261ms ``` -**Causes:** -- Proxy not running -- Wrong port or hostname -- Firewall blocking connection -- Service not listening on specified port - -**Solutions:** -- Start proxy service -- Verify correct hostname and port -- Check `docker ps` for running containers -- Use correct service URL -- Try a different proxy from the table above +## Health -### **3. Rate Limiting & Usage Issues** +```bash +mump2p health +``` -#### **Error: Rate limit exceeded** ``` -Error: per-hour limit reached (100/hour) -Error: daily quota exceeded -Error: message size exceeds limit +Proxy Health Status: +------------------- +Status: ok +Memory Used: 51.30% +CPU Used: 73.94% +Disk Used: 9.41% +Country: United States (US) ``` -**Causes:** -- Publishing too frequently -- Message too large for tier -- Daily quota exhausted -- Per-second limit hit +## List Topics -**Solutions:** -- Wait for rate limit reset -- Use smaller messages -- Check usage: `./mump2p usage` -- Contact admin for higher limits -- Spread out publish operations +```bash +mump2p list-topics +``` -#### **Error: Token expired** ``` -Error: token has expired, please login again +📋 Subscribed Topics for Client: auth0| +═══════════════════════════════════════════════════════════════ + Total Topics: 7 + + 1. test/adr2-cli + 2. test/cli-e2e + 3. test + 4. /eth2/c6ecb76c/beacon_block/ssz_snappy + 5. mump2p_aggregated_messages + 6. test/adr2-grpc + 7. test/domain-e2e +═══════════════════════════════════════════════════════════════ ``` -**Causes:** -- JWT token expired (24 hours) -- Clock skew -- Token corrupted +## Usage Stats -**Solutions:** -- Refresh token: `./mump2p refresh` -- Login again: `./mump2p login` -- Check system time +```bash +mump2p usage +``` +``` + Publish (hour): 2 / 50000 + Publish (second): 1 / 600 + Data Used: 0.0001 MB / 20480.0000 MB + Next Reset: 09 Mar 26 23:52 IST (23h58m10s from now) + Last Publish: 08 Mar 26 23:52 IST +``` +## Version -### **4. CLI Usage & Syntax Issues** +```bash +mump2p version +``` -#### **Error: Missing required flags** ``` -Error: required flag(s) "topic" not set +Version: v0.0.1-rc8 +Commit: 4f76630 ``` -**Causes:** -- Forgetting required command line arguments -- Typos in flag names +## Update -**Solutions:** -- Use `--help` to see required flags -- Include all required arguments -- Check flag spelling and syntax +```bash +mump2p update +``` -### **5. Development Mode (`--disable-auth`)** +## Without Auth (Testing) -For development and testing, you can bypass authentication: +All commands support `--disable-auth --client-id ` to skip Auth0. -```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" +```bash +mump2p subscribe --topic test --disable-auth --client-id my-test-user +mump2p publish --topic test --message "hello" --disable-auth --client-id my-test-user +mump2p list-topics --disable-auth --client-id my-test-user ``` -> **Note:** This mode is for testing only. No rate limits enforced. See [guide](./docs/guide.md) for full details. +## Output Formats -### **6. Debug Mode & Performance Analysis** +All read commands support `--output json` or `--output yaml`. -The `--debug` flag provides detailed timing and proxy information for troubleshooting: +```bash +mump2p whoami --output json +mump2p health --output yaml +mump2p list-topics --output json +``` -```sh -# Enable debug mode for operations -./mump2p --debug publish --topic=test-topic --message='Hello World' -./mump2p --debug subscribe --topic=test-topic +## Global Flags -# 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" -``` +| Flag | Description | +|------|-------------| +| `--auth-path` | Custom path for auth file (default: `~/.mump2p/auth.yml`) | +| `--client-id` | Client ID (required with `--disable-auth`) | +| `--debug` | Session detail, node scores, message IDs, peer paths, timing breakdowns | +| `--disable-auth` | Skip Auth0 for testing | +| `--expose-amount N` | Number of nodes to request for failover (subscribe default: `3`, publish default: `1`) | +| `--output` | Output format: `table`, `json`, `yaml` | -For comprehensive debug mode usage, performance analysis, and blast testing examples, see the [Complete User Guide](./docs/guide.md#debug-mode). +## Override Proxy ---- +Any command that talks to the proxy accepts `--service-url`: + +```bash +mump2p subscribe --topic test --service-url http://us2-proxy.getoptimum.io:8080 +mump2p publish --topic test --message "hi" --service-url http://us3-proxy.getoptimum.io:8080 +mump2p health --service-url http://us2-proxy.getoptimum.io:8080 +``` -**Pro Tips for First-Time Users:** -- Always check `docker ps` and `docker logs` for container status -- Use `--help` flag liberally to understand command options -- Test authentication first with `whoami` before trying other operations -- Start with simple publish/subscribe before advanced features -- Keep proxy and CLI logs visible during troubleshooting -- Check `usage` command regularly to monitor limits -- For webhook integration and advanced features, see the [Complete User Guide](./docs/guide.md) +Available proxies: +- `http://us1-proxy.getoptimum.io:8080` +- `http://us2-proxy.getoptimum.io:8080` +- `http://us3-proxy.getoptimum.io:8080` diff --git a/cmd/auth.go b/cmd/auth.go index bf1b26c..92f8966 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -6,6 +6,7 @@ import ( "github.com/getoptimum/mump2p-cli/internal/auth" "github.com/getoptimum/mump2p-cli/internal/formatter" + "github.com/getoptimum/mump2p-cli/internal/session" "github.com/spf13/cobra" ) @@ -65,6 +66,7 @@ var logoutCmd = &cobra.Command{ if err := storage.RemoveToken(); err != nil { return err } + session.InvalidateSession() fmt.Println("✅ Successfully logged out") return nil diff --git a/cmd/publish.go b/cmd/publish.go index 34ea667..16a1b60 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -7,60 +7,66 @@ import ( "encoding/json" "errors" "fmt" - "io" - "net/http" "os" - "strings" "time" "github.com/getoptimum/mump2p-cli/internal/auth" "github.com/getoptimum/mump2p-cli/internal/config" - grpcclient "github.com/getoptimum/mump2p-cli/internal/grpc" + "github.com/getoptimum/mump2p-cli/internal/node" "github.com/getoptimum/mump2p-cli/internal/ratelimit" + "github.com/getoptimum/mump2p-cli/internal/session" + pb "github.com/getoptimum/mump2p-cli/proto" "github.com/spf13/cobra" ) var ( - pubTopic string - pubMessage string - file string - //optional - serviceURL string - useGRPCPub bool // gRPC flag for publish + pubTopic string + pubMessage string + file string + serviceURL string + pubExposeAmount uint32 ) -// PublishPayload matches the expected JSON body on the server -type PublishRequest struct { - ClientID string `json:"client_id"` - Topic string `json:"topic"` - Message string `json:"message"` - Timestamp int64 `json:"timestamp"` -} - -// addDebugPrefix adds debug information prefix to message data -func addDebugPrefix(data []byte, proxyAddr string) []byte { +func addDebugPrefix(data []byte, addr string) []byte { currentTime := time.Now().UnixNano() - prefix := fmt.Sprintf("sender_addr:%s\t[send_time, size]:[%d, %d]\t", proxyAddr, currentTime, len(data)) - prefixBytes := []byte(prefix) - return append(prefixBytes, data...) + prefix := fmt.Sprintf("sender_addr:%s\t[send_time, size]:[%d, %d]\t", addr, currentTime, len(data)) + return append([]byte(prefix), data...) } -// printDebugInfo prints debug information for publish operations -func printDebugInfo(data []byte, proxyAddr string, topic string, isGRPC bool) { +func printDebugInfo(data []byte, addr string, topic string) { currentTime := time.Now().UnixNano() sum := sha256.Sum256(data) hash := hex.EncodeToString(sum[:]) - protocol := "HTTP" - if isGRPC { - protocol = "gRPC" + fmt.Printf("Publish:\tsender_info:%s, [send_time, size]:[%d, %d]\ttopic:%s\tmsg_hash:%s\tprotocol:gRPC-direct\n", + addr, currentTime, len(data), topic, hash[:8]) +} + +func shortMsgID(resp *pb.Response) string { + if resp == nil || len(resp.Data) == 0 { + return "" + } + var trace map[string]interface{} + if err := json.Unmarshal(resp.Data, &trace); err != nil { + return "" } - fmt.Printf("Publish:\tsender_info:%s, [send_time, size]:[%d, %d]\ttopic:%s\tmsg_hash:%s\tprotocol:%s\n", - proxyAddr, currentTime, len(data), topic, hash[:8], protocol) + if mid, ok := trace["messageID"].(string); ok && mid != "" { + if len(mid) > 8 { + return mid[:8] + } + return mid + } + if mid, ok := trace["message_id"].(string); ok && mid != "" { + if len(mid) > 8 { + return mid[:8] + } + return mid + } + return "" } var publishCmd = &cobra.Command{ Use: "publish", - Short: "Publish a message to the Optimum Network via HTTP or gRPC", + Short: "Publish a message to the Optimum Network", RunE: func(cmd *cobra.Command, args []string) error { if pubMessage == "" && file == "" { return errors.New("either --message or --file must be provided") @@ -70,39 +76,34 @@ var publishCmd = &cobra.Command{ } var claims *auth.TokenClaims - var token *auth.StoredToken var clientIDToUse string + var accessToken string if !IsAuthDisabled() { authClient := auth.NewClient() storage := auth.NewStorageWithPath(GetAuthPath()) - var err error - token, err = authClient.GetValidToken(storage) + token, err := authClient.GetValidToken(storage) if err != nil { return fmt.Errorf("authentication required: %v", err) } - // parse token to check if the account is active + accessToken = token.Token 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 - source string - ) + + var data []byte if file != "" { content, err := os.ReadFile(file) @@ -110,135 +111,131 @@ var publishCmd = &cobra.Command{ return fmt.Errorf("failed to read file: %v", err) } data = content - source = file } else { data = []byte(pubMessage) - source = "inline message" } - // message size + messageSize := int64(len(data)) - // 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 } } - // use custom service URL if provided, otherwise use the default - baseURL := config.LoadConfig().ServiceUrl + proxyURL := config.LoadConfig().ServiceUrl if serviceURL != "" { - baseURL = serviceURL + proxyURL = serviceURL } - if useGRPCPub { - // gRPC publish logic - grpcAddr := strings.Replace(baseURL, "http://", "", 1) - grpcAddr = strings.Replace(grpcAddr, "https://", "", 1) - // Replace the port with 50051 for gRPC (default gRPC port) - if strings.Contains(grpcAddr, ":") { - // Extract host part and append gRPC port - host := strings.Split(grpcAddr, ":")[0] - grpcAddr = host + ":50051" + sessionStart := time.Now() + sess, reused, err := session.GetOrCreateSession( + proxyURL, + clientIDToUse, + accessToken, + []string{pubTopic}, + []string{"publish"}, + pubExposeAmount, + ) + if err != nil { + return fmt.Errorf("session creation failed: %v", err) + } + sessionDur := time.Since(sessionStart) + + if IsDebugMode() { + if reused { + fmt.Printf("Reusing session %s | %d node(s) available\n", sess.SessionID, len(sess.Nodes)) } else { - grpcAddr += ":50051" // default port if not specified + fmt.Printf("New session %s from %s (%s) | %d node(s) available\n", + sess.SessionID, proxyURL, humanDuration(sessionDur), len(sess.Nodes)) } + } - // Extract proxy IP for debug mode - proxyAddr := extractIPFromURL(grpcAddr) - if proxyAddr == "" { - proxyAddr = grpcAddr // fallback to full address if no IP found + var published bool + for i, n := range sess.Nodes { + if IsDebugMode() { + fmt.Printf(" Trying node %d/%d: %s (%s, score: %.2f)...\n", + i+1, len(sess.Nodes), n.Address, n.Region, n.Score) + } + + nodeAddr := extractIPFromURL(n.Address) + if nodeAddr == "" { + nodeAddr = n.Address } - // Add debug prefix to data if debug mode is enabled publishData := data if IsDebugMode() { - publishData = addDebugPrefix(data, proxyAddr) + publishData = addDebugPrefix(data, nodeAddr) } - ctx := context.Background() - client, err := grpcclient.NewProxyClient(grpcAddr) - if err != nil { - return fmt.Errorf("failed to connect to gRPC proxy: %v", err) + connectStart := time.Now() + nc, connErr := node.NewClient(n.Address) + if connErr != nil { + fmt.Printf(" Node %s unreachable: %v\n", n.Address, connErr) + continue } - defer client.Close() //nolint:errcheck - err = client.Publish(ctx, clientIDToUse, pubTopic, publishData) - if err != nil { - return fmt.Errorf("gRPC publish failed: %v", err) + ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second) + resp, pubErr := nc.Publish(ctx, n.Ticket, pubTopic, publishData) + rpcDur := time.Since(connectStart) + ctxCancel() + nc.Close() + + if pubErr != nil { + fmt.Printf(" Node %s failed: %v\n", n.Address, pubErr) + continue } - // Print debug information if debug mode is enabled if IsDebugMode() { - printDebugInfo(publishData, proxyAddr, pubTopic, true) + printDebugInfo(publishData, nodeAddr, pubTopic) } - fmt.Println("✅ Published via gRPC", source) - } else { - // HTTP publish logic (existing) - // Extract proxy IP for debug mode - proxyAddr := extractIPFromURL(baseURL) - if proxyAddr == "" { - proxyAddr = baseURL // fallback to full URL if no IP found + region := n.Region + if region == "" { + region = "unknown" } - // Add debug prefix to data if debug mode is enabled - publishData := data - if IsDebugMode() { - publishData = addDebugPrefix(data, proxyAddr) + msgID := shortMsgID(resp) + suffix := "" + if msgID != "" { + suffix = fmt.Sprintf(" [msg: %s]", msgID) } - reqData := PublishRequest{ - ClientID: clientIDToUse, - Topic: pubTopic, - Message: string(publishData), // use modified data with debug prefix if enabled - Timestamp: time.Now().UnixMilli(), - } - reqBytes, err := json.Marshal(reqData) - if err != nil { - return fmt.Errorf("failed to marshal publish request: %v", err) + if IsDebugMode() && !reused { + fmt.Printf("Session: %s | Published: %s | Total: %s\n", + humanDuration(sessionDur), humanDuration(rpcDur), + humanDuration(sessionDur+rpcDur)) } - url := baseURL + "/api/v1/publish" - req, err := http.NewRequest("POST", url, strings.NewReader(string(reqBytes))) - if err != nil { - return err - } - // 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") + fmt.Printf("Published to %s (%s) in %s%s\n", + n.Address, region, humanDuration(rpcDur), suffix) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("HTTP publish failed: %v", err) - } - defer resp.Body.Close() //nolint:errcheck - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != 200 { - return fmt.Errorf("publish error: %s", string(body)) + if IsDebugMode() && resp != nil && msgID != "" { + var trace map[string]interface{} + if json.Unmarshal(resp.Data, &trace) == nil { + if mid, ok := trace["messageID"].(string); ok && mid != "" { + fmt.Printf(" message-id: %s\n", mid) + } else if mid, ok := trace["message_id"].(string); ok && mid != "" { + fmt.Printf(" message-id: %s\n", mid) + } + } } - // Print debug information if debug mode is enabled - if IsDebugMode() { - printDebugInfo(publishData, proxyAddr, pubTopic, false) - } + published = true + break + } - fmt.Println("✅ Published via HTTP", source) - fmt.Println(string(body)) + if !published { + return fmt.Errorf("all %d node(s) failed to publish", len(sess.Nodes)) } - // 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) + _ = limiter.RecordPublish(messageSize) } } return nil @@ -247,10 +244,10 @@ var publishCmd = &cobra.Command{ func init() { publishCmd.Flags().StringVar(&pubTopic, "topic", "", "Topic to publish to") - publishCmd.Flags().StringVar(&pubMessage, "message", "", "Message string to publish (max 10MB)") - publishCmd.Flags().StringVar(&file, "file", "", "Path of the file to publish (max 10MB)") - publishCmd.Flags().StringVar(&serviceURL, "service-url", "", "Override the default service URL") - publishCmd.Flags().BoolVar(&useGRPCPub, "grpc", false, "Use gRPC for publishing instead of HTTP") + publishCmd.Flags().StringVar(&pubMessage, "message", "", "Message string to publish") + publishCmd.Flags().StringVar(&file, "file", "", "Path of the file to publish") + publishCmd.Flags().StringVar(&serviceURL, "service-url", "", "Override the default proxy URL") + publishCmd.Flags().Uint32Var(&pubExposeAmount, "expose-amount", 1, "Number of nodes to request from proxy") publishCmd.MarkFlagRequired("topic") //nolint:errcheck rootCmd.AddCommand(publishCmd) } diff --git a/cmd/root.go b/cmd/root.go index 8832bf2..f870e31 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,25 +1,27 @@ package cmd import ( + "fmt" "os" "path/filepath" + "time" "github.com/spf13/cobra" ) var ( - 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) - outputFormat string // Global flag for output format (table, json, yaml) + authPath string + debug bool + disableAuth bool + clientID string + outputFormat string ) var rootCmd = &cobra.Command{ Use: "mump2p", - Short: "CLI to interact with Optimum directly via Go", - Long: `mump2p is a developer tool for interacting with Optimum -without relying on the HTTP server. It directly invokes Go services.`, + Short: "Direct P2P publish/subscribe on the Optimum Network", + Long: `mump2p connects you directly to the Optimum P2P network. +Publish and subscribe with direct node connections for real-time, low-latency messaging.`, } func Execute() { @@ -32,10 +34,7 @@ func init() { // Add global flag for custom authentication path rootCmd.PersistentFlags().StringVar(&authPath, "auth-path", os.Getenv("MUMP2P_AUTH_PATH"), "Custom path for authentication file (default: ~/.mump2p/auth.yml, env: MUMP2P_AUTH_PATH)") - // 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(&debug, "debug", false, "Enable debug mode with session detail, node scores, message IDs, and peer paths") rootCmd.PersistentFlags().BoolVar(&disableAuth, "disable-auth", false, "Disable authentication checks (for testing/development)") // Add global client ID flag @@ -81,3 +80,18 @@ func GetClientID() string { func GetOutputFormat() string { return outputFormat } + +func humanDuration(d time.Duration) string { + if d < time.Second { + return fmt.Sprintf("%dms", d.Milliseconds()) + } + if d < time.Minute { + return fmt.Sprintf("%.1fs", d.Seconds()) + } + m := int(d.Minutes()) + s := int(d.Seconds()) % 60 + if s == 0 { + return fmt.Sprintf("%dm", m) + } + return fmt.Sprintf("%dm%ds", m, s) +} diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 9e7fa66..5606f41 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -5,9 +5,7 @@ import ( "context" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" - "io" "net/http" "os" "os/signal" @@ -17,13 +15,15 @@ import ( "sync/atomic" "syscall" "time" + "unicode/utf8" "github.com/getoptimum/mump2p-cli/internal/auth" "github.com/getoptimum/mump2p-cli/internal/config" "github.com/getoptimum/mump2p-cli/internal/entities" - grpcsub "github.com/getoptimum/mump2p-cli/internal/grpc" + "github.com/getoptimum/mump2p-cli/internal/node" + "github.com/getoptimum/mump2p-cli/internal/session" "github.com/getoptimum/mump2p-cli/internal/webhook" - "github.com/gorilla/websocket" + pb "github.com/getoptimum/mump2p-cli/proto" "github.com/spf13/cobra" ) @@ -34,28 +34,16 @@ var ( webhookSchema string webhookQueueSize int webhookTimeoutSecs int - subThreshold float32 - //optional - subServiceURL string - useGRPC bool // <-- new flag - grpcBufferSize int // gRPC message buffer size + subServiceURL string + subExposeAmount uint32 ) -// SubscribeRequest represents the HTTP POST payload -type SubscribeRequest struct { - ClientID string `json:"client_id"` - Topic string `json:"topic"` - Threshold float32 `json:"threshold,omitempty"` -} - -// printDebugReceiveInfo prints debug information for received messages func printDebugReceiveInfo(message []byte, receiverAddr string, topic string, messageNum int32, protocol string) { currentTime := time.Now().UnixNano() messageSize := len(message) sum := sha256.Sum256(message) hash := hex.EncodeToString(sum[:]) - // Extract sender info from message if it contains debug prefix sendInfoRegex := regexp.MustCompile(`sender_addr:\d+\.\d+\.\d+\.\d+\t\[send_time, size\]:\[\d+,\s*\d+\]`) sendInfo := sendInfoRegex.FindString(string(message)) @@ -63,91 +51,91 @@ func printDebugReceiveInfo(message []byte, receiverAddr string, topic string, me messageNum, receiverAddr, currentTime, messageSize, sendInfo, topic, hash[:8], protocol) } -// decodeWebSocketMessage attempts to parse and decode the message. -// Returns decoded message bytes, or raw message for backward compatibility. -func decodeWebSocketMessage(rawMsg []byte) []byte { - p2pMsg, err := entities.UnmarshalP2PMessage(rawMsg) +func decodeMessage(rawMsg []byte) (decoded []byte, topic string, p2pMsg *entities.P2PMessage) { + msg, err := entities.UnmarshalP2PMessage(rawMsg) if err != nil { - return rawMsg + return rawMsg, "", nil } - return p2pMsg.Message + return msg.Message, msg.Topic, msg +} + +func isReadable(b []byte) bool { + for _, c := range b { + if c < 0x20 && c != '\n' && c != '\r' && c != '\t' { + return false + } + } + return len(b) > 0 && utf8.Valid(b) +} + +func formatMessage(data []byte) string { + if isReadable(data) { + return string(data) + } + if len(data) > 256 { + return fmt.Sprintf("[binary %d bytes] %x...", len(data), data[:128]) + } + return fmt.Sprintf("[binary %d bytes] %x", len(data), data) } var subscribeCmd = &cobra.Command{ Use: "subscribe", - Short: "Subscribe to a topic via WebSocket or gRPC stream", + Short: "Subscribe to a topic and stream messages from the P2P network", RunE: func(cmd *cobra.Command, args []string) error { - var claims *auth.TokenClaims - var token *auth.StoredToken var clientIDToUse string + var accessToken string if !IsAuthDisabled() { - // auth authClient := auth.NewClient() storage := auth.NewStorageWithPath(GetAuthPath()) - var err error - token, err = authClient.GetValidToken(storage) + token, err := authClient.GetValidToken(storage) if err != nil { return fmt.Errorf("authentication required: %v", err) } - // parse token to check if the account is active + accessToken = token.Token parser := auth.NewTokenParser() - claims, err = parser.ParseToken(token.Token) + 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 var persistFile *os.File if persistPath != "" { - // check if persistPath is a directory or ends with a directory separator fileInfo, err := os.Stat(persistPath) if err == nil && fileInfo.IsDir() || strings.HasSuffix(persistPath, "/") || strings.HasSuffix(persistPath, "\\") { - // If it's a directory, append a default filename persistPath = filepath.Join(persistPath, "messages.log") } - - // create directory if it doesn't exist if err := os.MkdirAll(filepath.Dir(persistPath), 0755); err != nil { return fmt.Errorf("failed to create persistence directory: %v", err) } - - // open file for appending persistFile, err = os.OpenFile(persistPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to open persistence file: %v", err) } - defer persistFile.Close() //nolint:errcheck - + defer persistFile.Close() fmt.Printf("Persisting data to: %s\n", persistPath) } - // validate webhook URL and schema if provided var webhookFormatter *webhook.TemplateFormatter if webhookURL != "" { if !strings.HasPrefix(webhookURL, "http://") && !strings.HasPrefix(webhookURL, "https://") { return fmt.Errorf("webhook URL must start with http:// or https://") } - - // Create template formatter formatter, err := webhook.NewTemplateFormatter(webhookSchema) if err != nil { return fmt.Errorf("invalid webhook schema: %v", err) } webhookFormatter = formatter - if webhookSchema == "" { fmt.Printf("Forwarding messages to webhook (raw format): %s\n", webhookURL) } else { @@ -155,308 +143,222 @@ var subscribeCmd = &cobra.Command{ } } - //signal handling for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - srcUrl := config.LoadConfig().ServiceUrl - // use custom service URL if provided, otherwise use the default + proxyURL := config.LoadConfig().ServiceUrl if subServiceURL != "" { - srcUrl = subServiceURL + proxyURL = subServiceURL } - // Prepare gRPC address if needed - var grpcAddr string - if useGRPC { - grpcAddr = strings.Replace(srcUrl, "http://", "", 1) - grpcAddr = strings.Replace(grpcAddr, "https://", "", 1) - // Replace the port with 50051 for gRPC (default gRPC port) - if strings.Contains(grpcAddr, ":") { - // Extract host part and append gRPC port - host := strings.Split(grpcAddr, ":")[0] - grpcAddr = host + ":50051" + sessionStart := time.Now() + sess, reused, err := session.GetOrCreateSession( + proxyURL, + clientIDToUse, + accessToken, + []string{subTopic}, + []string{"subscribe"}, + subExposeAmount, + ) + if err != nil { + return fmt.Errorf("session creation failed: %v", err) + } + sessionDur := time.Since(sessionStart) + + if IsDebugMode() { + if reused { + fmt.Printf("Reusing session %s | %d node(s) available\n", sess.SessionID, len(sess.Nodes)) } else { - grpcAddr += ":50051" // default port if not specified + fmt.Printf("New session %s from %s (%s) | %d node(s) available\n", + sess.SessionID, proxyURL, humanDuration(sessionDur), len(sess.Nodes)) } - fmt.Printf("Using gRPC service URL: %s\n", grpcAddr) - } else { - fmt.Printf("Using HTTP service URL: %s\n", srcUrl) } - // send subscription request (HTTP or gRPC based on useGRPC flag) - if useGRPC { - // Use gRPC for subscription request - fmt.Println("Sending gRPC subscription request...") - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - client, err := grpcsub.NewProxyClient(grpcAddr) - if err != nil { - return fmt.Errorf("failed to connect to gRPC proxy: %v", err) + var ( + nodeClient *node.Client + connectedNode session.Node + msgChan <-chan *pb.Response + ) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + connectStart := time.Now() + for i, n := range sess.Nodes { + if IsDebugMode() { + fmt.Printf(" Trying node %d/%d: %s (%s, score: %.2f)...\n", + i+1, len(sess.Nodes), n.Address, n.Region, n.Score) } - defer client.Close() //nolint:errcheck - err = client.SubscribeTopic(ctx, clientIDToUse, subTopic, subThreshold) - if err != nil { - return fmt.Errorf("gRPC subscribe failed: %v", err) + nc, connErr := node.NewClient(n.Address) + if connErr != nil { + fmt.Printf(" Node %s unreachable, falling back...\n", n.Address) + continue } - fmt.Printf("gRPC subscription successful: subscribed to topic '%s'\n", subTopic) - } else { - // Use HTTP for subscription request - fmt.Println("Sending HTTP POST subscription request...") - httpEndpoint := fmt.Sprintf("%s/api/v1/subscribe", srcUrl) - reqData := SubscribeRequest{ - ClientID: clientIDToUse, - Topic: subTopic, - Threshold: subThreshold, - } - reqBytes, err := json.Marshal(reqData) - if err != nil { - return fmt.Errorf("failed to marshal subscription request: %v", err) + ch, subErr := nc.Subscribe(ctx, n.Ticket, subTopic, 100) + if subErr != nil { + fmt.Printf(" Node %s subscribe failed, falling back...\n", n.Address) + nc.Close() + continue } - req, err := http.NewRequest("POST", httpEndpoint, bytes.NewBuffer(reqBytes)) - if err != nil { - return fmt.Errorf("failed to create HTTP request: %v", err) - } - // 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") + nodeClient = nc + connectedNode = n + msgChan = ch + break + } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("HTTP POST subscribe failed: %v", err) - } + if nodeClient == nil { + return fmt.Errorf("all %d node(s) failed to connect", len(sess.Nodes)) + } + defer nodeClient.Close() + connectDur := time.Since(connectStart) - defer resp.Body.Close() //nolint:errcheck - body, _ := io.ReadAll(resp.Body) + region := connectedNode.Region + if region == "" { + region = "unknown" + } - if resp.StatusCode != 200 { - return fmt.Errorf("HTTP POST subscribe error: %s", string(body)) + var backupNodes []session.Node + for _, n := range sess.Nodes { + if n.Address != connectedNode.Address { + backupNodes = append(backupNodes, n) } - - fmt.Printf("HTTP POST subscription successful: %s\n", string(body)) } - if useGRPC { - // gRPC streaming logic (reuse the connection from subscription) - // Extract receiver IP for debug mode - receiverAddr := extractIPFromURL(grpcAddr) - if receiverAddr == "" { - receiverAddr = grpcAddr // fallback to full address if no IP found - } + backupSuffix := "" + if len(backupNodes) == 1 { + backupSuffix = " — 1 backup node ready" + } else if len(backupNodes) > 1 { + backupSuffix = fmt.Sprintf(" — %d backup nodes ready", len(backupNodes)) + } - // Create a new context for streaming (separate from subscription context) - streamCtx, streamCancel := context.WithCancel(context.Background()) - defer streamCancel() + fmt.Printf("Subscribed to '%s' on %s (%s) in %s%s\n", + subTopic, connectedNode.Address, region, humanDuration(connectDur), backupSuffix) - // Create a new client connection for streaming - streamClient, err := grpcsub.NewProxyClient(grpcAddr) - if err != nil { - return fmt.Errorf("failed to connect to gRPC proxy for streaming: %v", err) + for _, bn := range backupNodes { + r := bn.Region + if r == "" { + r = "unknown" } - defer streamClient.Close() //nolint:errcheck + fmt.Printf(" backup: %s (%s)\n", bn.Address, r) + } - msgChan, err := streamClient.Subscribe(streamCtx, clientIDToUse, grpcBufferSize) - if err != nil { - return fmt.Errorf("gRPC stream subscribe failed: %v", err) - } + receiverAddr := extractIPFromURL(connectedNode.Address) + if receiverAddr == "" { + receiverAddr = connectedNode.Address + } - fmt.Printf("Listening for messages on topic '%s' via gRPC... Press Ctrl+C to exit\n", subTopic) + type webhookMsg struct { + data []byte + } - // webhook queue and worker (same as before) - type webhookMsg struct { - data []byte - } - webhookQueue := make(chan webhookMsg, webhookQueueSize) + var wq chan webhookMsg + if webhookURL != "" && webhookQueueSize > 0 { + wq = make(chan webhookMsg, webhookQueueSize) go func() { - for msg := range webhookQueue { + for msg := range wq { go func(payload []byte) { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(webhookTimeoutSecs)*time.Second) - defer cancel() + wctx, wcancel := context.WithTimeout(context.Background(), time.Duration(webhookTimeoutSecs)*time.Second) + defer wcancel() - // Format the payload using template - formattedPayload, err := webhookFormatter.FormatMessage(payload, subTopic, clientIDToUse, "grpc-msg") - if err != nil { - fmt.Printf("Failed to format webhook payload: %v\n", err) + formattedPayload, fmtErr := webhookFormatter.FormatMessage(payload, subTopic, clientIDToUse, "grpc-msg") + if fmtErr != nil { + fmt.Printf("Failed to format webhook payload: %v\n", fmtErr) return } - req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(formattedPayload)) - if err != nil { - fmt.Printf("Failed to create webhook request: %v\n", err) + req, reqErr := http.NewRequestWithContext(wctx, "POST", webhookURL, bytes.NewBuffer(formattedPayload)) + if reqErr != nil { + fmt.Printf("Failed to create webhook request: %v\n", reqErr) return } - req.Header.Set("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - fmt.Printf("Webhook request error: %v\n", err) + if webhookSchema != "" { + req.Header.Set("Content-Type", "application/json") + } + resp, doErr := http.DefaultClient.Do(req) + if doErr != nil { + fmt.Printf("Webhook request error: %v\n", doErr) return } - defer resp.Body.Close() //nolint:errcheck + defer resp.Body.Close() if resp.StatusCode >= 400 { fmt.Printf("Webhook responded with status code: %d\n", resp.StatusCode) } }(msg.data) } }() - - // receiver - doneChan := make(chan struct{}) - var messageCount int32 - go func() { - defer close(doneChan) - for msg := range msgChan { - decodedMsg := decodeWebSocketMessage(msg.Message) - msgStr := string(decodedMsg) - - // Print debug information if debug mode is enabled - if IsDebugMode() { - n := atomic.AddInt32(&messageCount, 1) - printDebugReceiveInfo(decodedMsg, receiverAddr, subTopic, n, "gRPC") - } else { - fmt.Println(msgStr) - } - - // persist - if persistFile != nil { - timestamp := time.Now().Format(time.RFC3339) - if _, err := fmt.Fprintf(persistFile, "[%s] %s\n", timestamp, msgStr); err != nil { - fmt.Printf("Error writing to persistence file: %v\n", err) - } - } - // forward - if webhookURL != "" { - select { - case webhookQueue <- webhookMsg{data: decodedMsg}: - default: - fmt.Println("⚠️ Webhook queue full, message dropped") - } - } - } - }() - - select { - case <-sigChan: - fmt.Println("\nClosing connection...") - streamCancel() - case <-doneChan: - fmt.Println("\nConnection closed by server") - } - return nil - } - - // setup ws connection - fmt.Println("Opening WebSocket connection...") - - // 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, clientIDToUse) - - // Extract receiver IP for debug mode - receiverAddr := extractIPFromURL(srcUrl) - if receiverAddr == "" { - receiverAddr = srcUrl // fallback to full URL if no IP found - } - - // setup ws headers for authentication - header := http.Header{} - // 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) - if err != nil { - return fmt.Errorf("websocket connection failed: %v", err) } - defer conn.Close() //nolint:errcheck - - fmt.Printf("Listening for messages on topic '%s'... Press Ctrl+C to exit\n", subTopic) - // webhook queue and worker - type webhookMsg struct { - data []byte - } - webhookQueue := make(chan webhookMsg, webhookQueueSize) - - go func() { - for msg := range webhookQueue { - go func(payload []byte) { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(webhookTimeoutSecs)*time.Second) - defer cancel() - - // Format the payload using template - formattedPayload, err := webhookFormatter.FormatMessage(payload, subTopic, clientIDToUse, "ws-msg") - if err != nil { - fmt.Printf("Failed to format webhook payload: %v\n", err) - return - } - - req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(formattedPayload)) - if err != nil { - fmt.Printf("Failed to create webhook request: %v\n", err) - return - } - req.Header.Set("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - fmt.Printf("Webhook request error: %v\n", err) - return - } - defer resp.Body.Close() //nolint:errcheck - - if resp.StatusCode >= 400 { - fmt.Printf("Webhook responded with status code: %d\n", resp.StatusCode) - } - }(msg.data) - } - }() - - // receiver doneChan := make(chan struct{}) var messageCount int32 + subscribeStart := time.Now() + go func() { defer close(doneChan) - for { - _, msg, err := conn.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - fmt.Printf("WebSocket read error: %v\n", err) + for resp := range msgChan { + if !IsDebugMode() { + switch resp.GetCommand() { + case pb.ResponseType_MessageTraceMumP2P, pb.ResponseType_MessageTraceGossipSub: + continue } - return } - decodedMsg := decodeWebSocketMessage(msg) - msgStr := string(decodedMsg) + decodedMsg, msgTopic, p2pMsg := decodeMessage(resp.Data) + + if msgTopic != "" && msgTopic != subTopic { + continue + } - // Print debug information if debug mode is enabled if IsDebugMode() { n := atomic.AddInt32(&messageCount, 1) - printDebugReceiveInfo(decodedMsg, receiverAddr, subTopic, n, "WebSocket") + printDebugReceiveInfo(decodedMsg, receiverAddr, subTopic, n, "gRPC-direct") + if p2pMsg != nil { + if p2pMsg.SourceNodeID != "" { + fmt.Printf(" from: %s\n", p2pMsg.SourceNodeID) + } + fmt.Printf(" via: %s (%s)\n", connectedNode.Address, region) + if p2pMsg.MessageID != "" { + id := p2pMsg.MessageID + if len(id) > 12 { + id = id[:12] + "..." + } + fmt.Printf(" id: %s\n", id) + } + } } else { - fmt.Println(msgStr) + if !isReadable(decodedMsg) { + continue + } + atomic.AddInt32(&messageCount, 1) + + displayTopic := subTopic + if msgTopic != "" { + displayTopic = msgTopic + } + fmt.Printf("[%s] %s\n", displayTopic, string(decodedMsg)) + } + + // Unreadable payloads are filtered from stdout in non-debug mode above; skip + // persistence and webhook for them in debug mode too (same as filtered topics). + if !isReadable(decodedMsg) { + continue } - // persist + msgStr := formatMessage(decodedMsg) + if persistFile != nil { timestamp := time.Now().Format(time.RFC3339) - if _, err := persistFile.WriteString(fmt.Sprintf("[%s] [%s] %s\n", timestamp, subTopic, msgStr)); err != nil { //nolint:staticcheck - fmt.Printf("Error writing to persistence file: %v\n", err) + if _, writeErr := fmt.Fprintf(persistFile, "[%s] %s\n", timestamp, msgStr); writeErr != nil { + fmt.Printf("Error writing to persistence file: %v\n", writeErr) } } - // forward - if webhookURL != "" { + if wq != nil { select { - case webhookQueue <- webhookMsg{data: decodedMsg}: + case wq <- webhookMsg{data: decodedMsg}: default: - fmt.Println("⚠️ Webhook queue full, message dropped") + fmt.Println("Webhook queue full, message dropped") } } } @@ -464,13 +366,23 @@ var subscribeCmd = &cobra.Command{ select { case <-sigChan: - fmt.Println("\nClosing connection...") - err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - if err != nil { - return fmt.Errorf("error closing connection: %v", err) - } + cancel() case <-doneChan: - fmt.Println("\nConnection closed by server") + } + + elapsed := time.Since(subscribeStart) + count := atomic.LoadInt32(&messageCount) + + throughput := "" + if elapsed > time.Second && count > 0 { + rate := float64(count) / elapsed.Seconds() + throughput = fmt.Sprintf(" (%.1f msg/s)", rate) + } + + if count == 1 { + fmt.Printf("\nDisconnected — 1 message in %s%s\n", humanDuration(elapsed), throughput) + } else { + fmt.Printf("\nDisconnected — %d messages in %s%s\n", count, humanDuration(elapsed), throughput) } return nil @@ -482,12 +394,10 @@ func init() { subscribeCmd.MarkFlagRequired("topic") //nolint:errcheck subscribeCmd.Flags().StringVar(&persistPath, "persist", "", "Path to file where messages will be stored") subscribeCmd.Flags().StringVar(&webhookURL, "webhook", "", "URL to forward messages to") - subscribeCmd.Flags().StringVar(&webhookSchema, "webhook-schema", "", "JSON template for webhook payload (e.g., '{\"content\":\"{{.Message}}\"}')") + subscribeCmd.Flags().StringVar(&webhookSchema, "webhook-schema", "", "JSON template for webhook payload") subscribeCmd.Flags().IntVar(&webhookQueueSize, "webhook-queue-size", 100, "Max number of webhook messages to queue before dropping") subscribeCmd.Flags().IntVar(&webhookTimeoutSecs, "webhook-timeout", 3, "Timeout in seconds for each webhook POST request") - subscribeCmd.Flags().Float32Var(&subThreshold, "threshold", 0.1, "Delivery threshold (0.1 to 1.0)") - subscribeCmd.Flags().StringVar(&subServiceURL, "service-url", "", "Override the default service URL") - subscribeCmd.Flags().BoolVar(&useGRPC, "grpc", false, "Use gRPC stream for subscription instead of WebSocket") - subscribeCmd.Flags().IntVar(&grpcBufferSize, "grpc-buffer-size", 100, "gRPC message buffer size (default: 100)") + subscribeCmd.Flags().StringVar(&subServiceURL, "service-url", "", "Override the default proxy URL") + subscribeCmd.Flags().Uint32Var(&subExposeAmount, "expose-amount", 3, "Number of nodes to request from proxy (enables failover if >1)") rootCmd.AddCommand(subscribeCmd) } diff --git a/docs/guide.md b/docs/guide.md index 861a6c6..8c2316a 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,6 +1,6 @@ # mump2p CLI - Complete User Guide -*This guide assumes you've completed the [Quick Start](../README.md#quick-start) from the README and are ready to explore advanced features, detailed configuration, and best practices.* +*This guide assumes you've completed the [Quick Start](../README.md) from the README and are ready to explore advanced features, detailed configuration, and best practices.* ## What You'll Learn @@ -8,8 +8,8 @@ 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 +- **Service Configuration**: Using different proxy servers and custom URLs +- **Direct P2P**: How messages flow directly to nodes via ADR-0002 - **Advanced Features**: Message persistence, webhooks, and monitoring - **Production Best Practices**: Performance optimization and troubleshooting @@ -18,11 +18,11 @@ After completing the README's quick start, this guide will teach you: ## Prerequisites Before following this guide, ensure you have: -- ✅ **Installed the CLI** via the [install script](../README.md#1-installation) or manual download -- ✅ **Completed authentication** with `./mump2p login` +- ✅ **Installed the CLI** via the install script or manual download +- ✅ **Completed authentication** with `mump2p login` - ✅ **Tested basic publish/subscribe** from the README examples -*If you haven't done these steps yet, please start with the [README Quick Start](../README.md#quick-start) first.* +*If you haven't done these steps yet, please start with the [README](../README.md) first.* --- @@ -35,38 +35,21 @@ Before following this guide, ensure you have: Check your current authentication status and token details: ```sh -./mump2p whoami +mump2p whoami ``` This displays: - Your client ID and email - Token expiration time (24 hours from login) -- Token validity status +- Token validity status - Rate limits and quotas for your account -**Example output:** -```text -Authentication Status: ----------------------- -Client ID: google-oauth2|100677750055416883405 -Expires: 24 Sep 25 20:44 IST -Valid for: 706h53m0s -Is Active: true - -Rate Limits: ------------- -Publish Rate: 1000 per hour -Publish Rate: 8 per second -Max Message Size: 4.00 MB -Daily Quota: 5120.00 MB -``` - ### Refresh Token If your token is about to expire, you can refresh it: ```sh -./mump2p refresh +mump2p refresh ``` ### Custom Authentication File Location @@ -75,19 +58,19 @@ By default, authentication tokens are stored in `~/.mump2p/auth.yml`. For produc ```sh # Use custom authentication file path -./mump2p --auth-path /opt/mump2p/auth/token.yml login +mump2p --auth-path /opt/mump2p/auth/token.yml login # All subsequent commands will use the same custom path -./mump2p --auth-path /opt/mump2p/auth/token.yml publish --topic=demo --message="Hello" -./mump2p --auth-path /opt/mump2p/auth/token.yml logout +mump2p --auth-path /opt/mump2p/auth/token.yml publish --topic=demo --message="Hello" +mump2p --auth-path /opt/mump2p/auth/token.yml logout ``` **Environment Variable Support:** ```sh # Set via environment variable (applies to all commands) export MUMP2P_AUTH_PATH="/opt/mump2p/auth/token.yml" -./mump2p login -./mump2p publish --topic=demo --message="Hello" +mump2p login +mump2p publish --topic=demo --message="Hello" ``` **Use Cases:** @@ -107,18 +90,14 @@ For development and testing scenarios, you can bypass authentication entirely us ```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" publish --topic=test --message="Hello" --service-url="http://34.146.222.111:8080" --grpc -./mump2p --disable-auth --client-id="my-test-client" subscribe --topic=test --service-url="http://34.146.222.111:8080" --grpc +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://us1-proxy.getoptimum.io:8080" +mump2p --disable-auth --client-id="my-test-client" subscribe --topic=test --service-url="http://us1-proxy.getoptimum.io:8080" +mump2p --disable-auth --client-id="my-test-client" list-topics --service-url="http://us1-proxy.getoptimum.io:8080" +mump2p --disable-auth usage # 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" +mump2p --disable-auth --client-id="my-test-client" --debug publish --topic=test --message="Hello" --service-url="http://us1-proxy.getoptimum.io:8080" ``` **When using `--disable-auth`:** @@ -127,14 +106,13 @@ For development and testing scenarios, you can bypass authentication entirely us - 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: ```sh -./mump2p logout +mump2p logout ``` --- @@ -143,95 +121,46 @@ To remove your stored authentication token: *The README used the default proxy. This section shows how to configure different proxy servers for better performance or geographic proximity.* -The CLI connects to different proxy servers around the world. By default, it uses the first available proxy, but you can specify a different one using the `--service-url` flag for better performance or closer geographic location. +The CLI connects to proxy servers for session management, then communicates directly with P2P nodes. By default, it uses `http://us1-proxy.getoptimum.io:8080`, but you can specify a different one using the `--service-url` flag. -For a complete list of available proxies and their locations, see: [Available Service URLs](../README.md#available-service-urls) in the README. +**Available proxies:** +- `http://us1-proxy.getoptimum.io:8080` +- `http://us2-proxy.getoptimum.io:8080` +- `http://us3-proxy.getoptimum.io:8080` **Example using a specific proxy:** ```sh -./mump2p publish --topic=test --message='Hello' --service-url="http://35.221.118.95:8080" -./mump2p subscribe --topic=test --service-url="http://34.142.205.26:8080" -``` - -**Example output when using custom service URL:** -```text -Using custom service URL: http://34.142.205.26:8080 -✅ Published via HTTP inline message -{"message_id":"f5f51132c83da5a0209d6348bffe7eb1dafc91544e9240b98ac2c8e6da25c410","status":"published","topic":"demo"} +mump2p publish --topic=test --message='Hello' --service-url="http://us2-proxy.getoptimum.io:8080" +mump2p subscribe --topic=test --service-url="http://us3-proxy.getoptimum.io:8080" ``` --- ## Subscribing to Messages - Deep Dive -*You've already tried basic topic subscription from the README. This section covers advanced options, protocols, and configuration.* - -### Understanding WebSocket vs gRPC - -The README showed you both protocols. Here's when to use each: - -**WebSocket (Default)** - Good for: -- Getting started and testing -- Lower resource usage -- Standard web-compatible streaming - -**gRPC** - Best for: -- High-throughput scenarios (1000+ messages/sec) -- Production environments -- Better performance and reliability - -### Basic WebSocket Subscription - -You've seen this from the README: - -```sh -./mump2p subscribe --topic=your-topic-name -``` - -This will open a WebSocket connection and display incoming messages in real-time. Press `Ctrl+C` to exit. +*You've already tried basic topic subscription from the README. This section covers advanced options and configuration.* -**Example output (WebSocket):** -```text -Using custom service URL: http://34.142.205.26:8080 -Sending HTTP POST subscription request... -HTTP POST subscription successful: {"client":"google-oauth2|100677750055416883405","status":"subscribed"} -Opening WebSocket connection... -Listening for messages on topic 'demo'... Press Ctrl+C to exit -``` +### How Subscribe Works (ADR-0002) -### gRPC Subscription (Advanced) +1. CLI requests a session from the proxy (control plane) +2. Proxy returns a list of scored P2P nodes with JWT tickets +3. CLI connects directly to the best node via gRPC +4. Messages stream directly from the node — no proxy hop -From the README, you saw the `--grpc` flag. Here's the detailed breakdown: +### Basic Subscription ```sh -./mump2p subscribe --topic=your-topic-name --grpc +mump2p subscribe --topic=your-topic-name ``` -**Example output:** -```text -Using custom service URL: http://34.142.205.26:8080 -Sending HTTP POST subscription request... -HTTP POST subscription successful: {"client":"google-oauth2|100677750055416883405","status":"subscribed"} -Listening for messages on topic 'demo' via gRPC... Press Ctrl+C to exit -``` - -gRPC provides: -- **Better performance** than WebSocket for high-throughput scenarios -- **Binary protocol** with smaller message overhead -- **Bidirectional streaming** support -- **Built-in retry and error handling** +By default, 3 nodes are requested for automatic failover. If the primary node fails, the CLI falls back to the next one. ### Save Messages to a File To persist messages to a local file while subscribing: ```sh -./mump2p subscribe --topic=your-topic-name --persist=/path/to/save/ -``` - -With gRPC: -```sh -./mump2p subscribe --topic=your-topic-name --persist=/path/to/save/ --grpc +mump2p subscribe --topic=your-topic-name --persist=/path/to/save/ ``` If you provide just a directory path, messages will be saved to a file named `messages.log` in that directory. @@ -241,12 +170,7 @@ If you provide just a directory path, messages will be saved to a file named `me To forward messages to an HTTP webhook: ```sh -./mump2p subscribe --topic=your-topic-name --webhook=https://your-server.com/webhook -``` - -With gRPC: -```sh -./mump2p subscribe --topic=your-topic-name --webhook=https://your-server.com/webhook --grpc +mump2p subscribe --topic=your-topic-name --webhook=https://your-server.com/webhook ``` **Note: The webhook endpoint must be configured to accept POST requests.** @@ -264,40 +188,22 @@ The CLI supports flexible JSON template formatting for webhooks. You can define **Discord Webhooks:** ```sh -./mump2p subscribe --topic=alerts --webhook="https://discord.com/api/webhooks/123456789/abcdef" --webhook-schema='{"content":"{{.Message}}"}' +mump2p subscribe --topic=alerts --webhook="https://discord.com/api/webhooks/123456789/abcdef" --webhook-schema='{"content":"{{.Message}}"}' ``` -- Messages are formatted as: `{"content": "your message"}` **Slack Webhooks:** ```sh -./mump2p subscribe --topic=notifications --webhook="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" --webhook-schema='{"text":"{{.Message}}"}' +mump2p subscribe --topic=notifications --webhook="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" --webhook-schema='{"text":"{{.Message}}"}' ``` -- Messages are formatted as: `{"text": "your message"}` **Telegram Webhooks:** ```sh -./mump2p subscribe --topic=alerts --webhook="https://api.telegram.org/bot/sendMessage" --webhook-schema='{"chat_id":"","text":"{{.Message}}"}' +mump2p subscribe --topic=alerts --webhook="https://api.telegram.org/bot/sendMessage" --webhook-schema='{"chat_id":"","text":"{{.Message}}"}' ``` -- Messages are formatted as: `{"chat_id": "123456789", "text": "your message"}` -- Requires bot token from @BotFather and chat ID from getUpdates API - -**Complex JSON Templates:** -```sh -./mump2p subscribe --topic=logs --webhook="https://your-server.com/webhook" --webhook-schema='{"message":"{{.Message}}","timestamp":"{{.Timestamp}}","topic":"{{.Topic}}","client":"{{.ClientID}}"}' -``` -- Messages are formatted with multiple fields and metadata **Raw Messages (No Schema):** ```sh -./mump2p subscribe --topic=logs --webhook="https://webhook.site/your-unique-id" -``` -- Messages are sent as raw content (no JSON formatting) -- Used for custom endpoints or testing services - -**Example Output:** -```text -Forwarding messages to webhook (custom schema): https://discord.com/api/webhooks/123456789/abcdef -Forwarding messages to webhook (raw format): https://webhook.site/your-unique-id +mump2p subscribe --topic=logs --webhook="https://webhook.site/your-unique-id" ``` #### Advanced Webhook Options @@ -305,11 +211,11 @@ Forwarding messages to webhook (raw format): https://webhook.site/your-unique-id For more control over webhook behavior: ```sh -./mump2p subscribe --topic=your-topic-name \ +mump2p subscribe --topic=your-topic-name \ --webhook=https://your-server.com/webhook \ --webhook-queue-size=200 \ --webhook-timeout=5 - ``` +``` Options: @@ -321,70 +227,29 @@ Options: You can both save messages locally and forward them to a webhook: ```sh -./mump2p subscribe --topic=your-topic-name \ +mump2p subscribe --topic=your-topic-name \ --persist=/path/to/messages.log \ --webhook=https://your-server.com/webhook ``` -With gRPC: -```sh -./mump2p subscribe --topic=your-topic-name \ - --persist=/path/to/messages.log \ - --webhook=https://your-server.com/webhook \ - --grpc -``` - --- ## Publishing Messages - Deep Dive -*You've tried basic publishing from the README. This section covers advanced publishing options, protocols, and file handling.* - -### HTTP Publishing (From README) +*You've tried basic publishing from the README. This section covers advanced publishing options and file handling.* -You've already used this basic command: +### Inline Publishing ```sh -./mump2p publish --topic=your-topic-name --message='Your message content' -``` - -**Example output:** -```text -✅ Published via HTTP inline message -{"message_id":"9cbf2612dd4371d154babad4e7b88e1f98b34cdf740283a406600f0959bdffff","status":"published","topic":"demo"} -``` - -### gRPC Publishing (Advanced) - -From the README, you saw the `--grpc` flag for publishing. Here's when and how to use it: - -```sh -./mump2p publish --topic=your-topic-name --message='Your message content' --grpc -``` - -**Example output:** -```text -✅ Published via gRPC inline message -``` - -**With custom service URL:** -```text -Using custom service URL: http://34.142.205.26:8080 -✅ Published via gRPC inline message +mump2p publish --topic=your-topic-name --message='Your message content' ``` ### Publish a File -To publish the contents of a file (HTTP): - -```sh -./mump2p publish --topic=your-topic-name --file=/path/to/your/file.json -``` - -To publish a file via gRPC: +To publish the contents of a file: ```sh -./mump2p publish --topic=your-topic-name --file=/path/to/your/file.json --grpc +mump2p publish --topic=your-topic-name --file=/path/to/your/file.json ``` Rate limits will be automatically applied based on your authentication token. @@ -398,35 +263,7 @@ Rate limits will be automatically applied based on your authentication token. To see all topics you're currently subscribed to: ```sh -./mump2p list-topics -``` - -This will display: - -- Your client ID -- Total number of active topics -- List of all subscribed topics with numbering -- Helpful guidance if no topics are found - -**Example output with topics:** -```text -📋 Subscribed Topics for Client: google-oauth2|116937893938826513819 -═══════════════════════════════════════════════════════════════ - Total Topics: 3 - - 1. test-topic-1 - 2. demo-topic - 3. news-updates -═══════════════════════════════════════════════════════════════ -``` - -**Example output with no topics:** -```text -📋 Subscribed Topics for Client: google-oauth2|116937893938826513819 -═══════════════════════════════════════════════════════════════ - No active topics found. - Use './mump2p subscribe --topic=' to subscribe to a topic. -═══════════════════════════════════════════════════════════════ +mump2p list-topics ``` ### List Topics from Different Proxy @@ -434,20 +271,7 @@ This will display: You can check your topics on a specific proxy server: ```sh -./mump2p list-topics --service-url="http://35.221.118.95:8080" -``` - -**Example output:** -```text -Using custom service URL: http://35.221.118.95:8080 - -📋 Subscribed Topics for Client: google-oauth2|116937893938826513819 -═══════════════════════════════════════════════════════════════ - Total Topics: 2 - - 1. production-topic - 2. monitoring-topic -═══════════════════════════════════════════════════════════════ +mump2p list-topics --service-url="http://us2-proxy.getoptimum.io:8080" ``` **Note:** Each proxy server maintains separate topic states, so you may have different topics on different proxies. @@ -457,23 +281,15 @@ Using custom service URL: http://35.221.118.95:8080 To see your current usage statistics and rate limits: ```sh -./mump2p usage +mump2p usage ``` -This will display: - -- Number of publish operations (per hour and per second) -- Data usage (bytes, KB, or MB depending on amount) -- Quota limits -- Time until usage counters reset -- Timestamps of your last publish and subscribe operations - ## Tracer Dashboard Interactive real-time dashboard showing network metrics, message statistics, and latency data. ```sh -./mump2p tracer dashboard +mump2p tracer dashboard ``` **Options:** @@ -482,205 +298,75 @@ Interactive real-time dashboard showing network metrics, message statistics, and - `--count`: Number of messages to auto-publish (default: `60`) - `--interval-ms`: Interval between published messages in ms (default: `500`) -**Example:** -```sh -./mump2p tracer dashboard --topic=metrics --count=100 --interval-ms=200 -``` - Press `q` or `Ctrl+C` to exit. -![Tracer Visualization](img/tracer.png) - ## Health Monitoring ### Check Proxy Server Health -To monitor the health and system metrics of the proxy server you're connected to: - ```sh -./mump2p health -``` - -This will display: - -- **Status**: Current health status of the proxy ("ok" if healthy) -- **Memory Used**: Memory usage percentage -- **CPU Used**: CPU usage percentage -- **Disk Used**: Disk usage percentage - -**Example output:** - -```text -Proxy Health Status: -------------------- -Status: ok -Memory Used: 7.02% -CPU Used: 0.25% -Disk Used: 44.05% +mump2p health ``` ### Check Health of Specific Proxy -You can check the health of a specific proxy server: - ```sh -./mump2p health --service-url="http://35.221.118.95:8080" +mump2p health --service-url="http://us2-proxy.getoptimum.io:8080" ``` -This is useful for: -- Monitoring multiple proxy servers -- Checking proxy health before switching service URLs -- Troubleshooting connection issues -- Performance monitoring and capacity planning - --- ## Debug Mode -The `--debug` flag provides detailed timing and proxy information for troubleshooting and performance analysis. When enabled, it shows: +The `--debug` flag provides detailed session, node, and timing information for troubleshooting and performance analysis. When enabled, it shows: -- **Message timestamps**: Send and receive times in nanoseconds -- **Proxy IP addresses**: Source and destination proxy information -- **Message metadata**: Size, hash, and protocol information -- **Message numbering**: Sequential numbering for received messages +- **Session details**: Session ID, proxy URL, session creation timing +- **Node selection**: Node addresses, regions, scores, connection attempts +- **Message metadata**: Timestamps, size, hash, protocol, P2P peer paths +- **Timing breakdown**: Session vs publish/subscribe timing ### Basic Debug Usage ```sh -# Debug publish operations -./mump2p --debug publish --topic=test-topic --message='Hello World' +# Debug publish +mump2p --debug publish --topic=test-topic --message='Hello World' -# Debug subscribe operations -./mump2p --debug subscribe --topic=test-topic - -# Debug with gRPC -./mump2p --debug publish --topic=test-topic --message='Hello World' --grpc -./mump2p --debug subscribe --topic=test-topic --grpc -``` - -### Debug Output Format - -**Publish Debug Output:** -```text -Publish: sender_info:34.146.222.111, [send_time, size]:[1757606701424811000, 2010] topic:test-topic msg_hash:4bbac12f protocol:HTTP +# Debug subscribe +mump2p --debug subscribe --topic=test-topic ``` -**Subscribe Debug Output:** -```text -Recv: [1] receiver_addr:34.146.222.111 [recv_time, size]:[1757606701424811000, 2082] sender_addr:34.146.222.111 [send_time, size]:[1757606700160514000, 2009] topic:test-topic hash:8da69366 protocol:WebSocket -``` - -### Debug Information Explained - -- **sender_info/receiver_addr**: IP address of the proxy handling the message -- **send_time/recv_time**: Unix timestamp in nanoseconds when message was sent/received -- **size**: Message size in bytes (includes debug prefix for received messages) -- **msg_hash/hash**: First 8 characters of SHA256 hash for message identification -- **protocol**: Communication protocol used (HTTP, gRPC, or WebSocket) -- **[n]**: Sequential message number for received messages +### Load Testing with Debug Mode -### Load Testing with Debug Mode (Blast Testing) +Debug mode is useful for load testing and performance analysis: -Debug mode is particularly useful for load testing and performance analysis. You can send multiple messages rapidly to measure throughput and latency: - -**Basic Blast Testing:** ```sh # Terminal 1: Subscribe with debug mode -./mump2p --debug subscribe --topic=load-test --service-url="http://34.146.222.111:8080" +mump2p --debug subscribe --topic=load-test # Terminal 2: Send multiple messages rapidly for i in {1..50}; do - ./mump2p --debug publish --topic=load-test --message="Test message $i" --service-url="http://34.146.222.111:8080" -done -``` - -**Advanced Blast Testing with gRPC:** -```sh -# Terminal 1: Subscribe via gRPC with debug mode -./mump2p --debug subscribe --topic=grpc-load-test --grpc --service-url="http://34.146.222.111:8080" - -# Terminal 2: Send 500 messages via gRPC -for i in {1..500}; do - ./mump2p --debug publish --topic=grpc-load-test --message="GRPC test message $i" --grpc --service-url="http://34.146.222.111:8080" + mump2p --debug publish --topic=load-test --message="Test message $i" done ``` -**Cross-Proxy Blast Testing:** -```sh -# Terminal 1: Subscribe on one proxy -./mump2p --debug subscribe --topic=cross-proxy-test --service-url="http://34.146.222.111:8080" - -# Terminal 2: Publish from different proxy (use a working proxy URL) -for i in {1..100}; do - ./mump2p --debug publish --topic=cross-proxy-test --message="Cross-proxy message $i" --service-url="http://34.146.222.111:8080" -done -``` - -**Analyzing Blast Test Results:** - -The debug output provides valuable metrics: -- **Throughput**: Count messages per second by analyzing timestamps -- **Latency**: Calculate `recv_time - send_time` for each message -- **Message Integrity**: Verify hashes match between send and receive -- **Proxy Performance**: Compare different proxy servers under load - -**Example Blast Test Output Analysis:** -```text -# Sending 10 messages rapidly -Publish: sender_info:34.146.222.111, [send_time, size]:[1757606701424811000, 2010] topic:load-test msg_hash:4bbac12f protocol:HTTP -Publish: sender_info:34.146.222.111, [send_time, size]:[1757606701424812000, 2010] topic:load-test msg_hash:5ccbd23g protocol:HTTP -... - -# Receiving messages with timing -Recv: [1] receiver_addr:34.146.222.111 [recv_time, size]:[1757606701424811000, 2082] sender_addr:34.146.222.111 [send_time, size]:[1757606701424811000, 2009] topic:load-test hash:4bbac12f protocol:WebSocket -Recv: [2] receiver_addr:34.146.222.111 [recv_time, size]:[1757606701424812000, 2082] sender_addr:34.146.222.111 [send_time, size]:[1757606701424812000, 2009] topic:load-test hash:5ccbd23g protocol:WebSocket -``` - -### Use Cases for Debug Mode - -- **Performance Analysis**: Measure message latency and throughput -- **Troubleshooting**: Identify proxy routing and timing issues -- **Message Tracking**: Verify message integrity using hashes -- **Cross-Proxy Testing**: Monitor message flow between different proxy servers -- **Load Testing**: Analyze performance under high message volumes -- **Blast Testing**: Rapid message sending for stress testing and performance benchmarking +--- ## Tips for Effective Use 1. **Topic Names:** Choose descriptive and unique topic names to avoid message conflicts 2. **Message Size:** Be aware of your maximum message size limit when publishing files 3. **Token Refresh:** For long-running operations, refresh your token before it expires -4. **Topic Management:** Use `./mump2p list-topics` to check your active topics and avoid duplicate topic subscriptions -5. **Persistent Subscriptions:** Use the --persist option when you need a record of messages +4. **Topic Management:** Use `mump2p list-topics` to check your active topics +5. **Persistent Subscriptions:** Use the `--persist` option when you need a record of messages 6. **Webhook Reliability:** Increase the queue size for high-volume topics to prevent message drops -7. **gRPC Performance:** Use `--grpc` flag for high-throughput scenarios and better performance -8. **Health Monitoring:** Check proxy health with `./mump2p health` before long operations -9. **Multi-Proxy Usage:** Remember that each proxy server maintains separate topic states - use `./mump2p list-topics --service-url=` to check topics on specific proxies -10. **Debug Analysis:** Use `--debug` flag for performance monitoring and troubleshooting message flow issues +7. **Failover:** Use `--expose-amount` to control how many backup nodes are available (subscribe defaults to 3) +8. **Health Monitoring:** Check proxy health with `mump2p health` before long operations +9. **Debug Analysis:** Use `--debug` flag for performance monitoring and troubleshooting message flow issues ## Troubleshooting -For common setup and usage issues, see the [FAQ section in the README](../README.md#faq---common-issues--troubleshooting). - -**Advanced troubleshooting:** - -- **Authentication Errors:** Run `./mump2p whoami` to check token status, and `./mump2p login` to re-authenticate -- **Rate Limit Errors:** Use `./mump2p usage` to check your current usage against limits -- **Topic Issues:** - - Use `./mump2p list-topics` to verify your active topics - - Check topics on different proxies with `./mump2p list-topics --service-url=` - - Remember that topics persist across logout/login sessions -- **Connection Issues:** - - Verify your internet connection and firewall settings - - Check proxy server health with `./mump2p health` - - Try a different proxy server with `--service-url` flag -- **Proxy Health Issues:** Use `./mump2p health` to check system metrics and server status -- **Webhook Failures:** - - Check that your webhook endpoint is accessible and properly configured to accept POST requests - - For Discord webhooks: Use `--webhook-schema='{"content":"{{.Message}}"}'` and ensure the webhook URL is valid - - For Slack webhooks: Use `--webhook-schema='{"text":"{{.Message}}"}'` and verify the webhook URL is correct - - For Telegram webhooks: Use `--webhook-schema='{"chat_id":"YOUR_CHAT_ID","text":"{{.Message}}"}'` with bot token and chat ID - - Check webhook response status codes - 400 errors usually indicate formatting issues (use appropriate schema) - - Use [webhook.site](https://webhook.site/) for testing generic webhook endpoints - - Define custom JSON templates with `--webhook-schema` for any webhook service - +- **Authentication Errors:** Run `mump2p whoami` to check token status, and `mump2p login` to re-authenticate +- **Rate Limit Errors:** Use `mump2p usage` to check your current usage against limits +- **Topic Issues:** Use `mump2p list-topics` to verify your active topics +- **Connection Issues:** Check proxy server health with `mump2p health`, try a different proxy with `--service-url` +- **Webhook Failures:** Check that your webhook endpoint is accessible and properly configured to accept POST requests diff --git a/e2e/cli_runner.go b/e2e/cli_runner.go deleted file mode 100644 index 0cd0156..0000000 --- a/e2e/cli_runner.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "bytes" - "context" - "os" - "os/exec" - "time" -) - -const ( - // DefaultCommandTimeout is the timeout for CLI commands in fuzz tests - // This prevents hanging when fuzzing creates valid URLs that don't respond - DefaultCommandTimeout = 10 * time.Second -) - -// RunCommand executes the CLI binary with given arguments and returns output -// It uses a timeout to prevent hanging during fuzz tests -func RunCommand(bin string, args ...string) (string, error) { - return RunCommandWithTimeout(bin, DefaultCommandTimeout, args...) -} - -// RunCommandWithTimeout executes the CLI binary with a specific timeout -func RunCommandWithTimeout(bin string, timeout time.Duration, args ...string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - var out bytes.Buffer - var stderr bytes.Buffer - - cmd := exec.CommandContext(ctx, bin, args...) - cmd.Env = os.Environ() - cmd.Stdout = &out - cmd.Stderr = &stderr - - err := cmd.Run() - if err != nil { - // Check if timeout was exceeded - if ctx.Err() == context.DeadlineExceeded { - return stderr.String(), context.DeadlineExceeded - } - return stderr.String(), err - } - return out.String(), nil -} diff --git a/e2e/commands_test.go b/e2e/commands_test.go deleted file mode 100644 index dc81174..0000000 --- a/e2e/commands_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestCLISmokeCommands(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - for _, tc := range smokeTestCases() { - t.Run(tc.Name, func(t *testing.T) { - output := runCLICommand(t, tc.Args...) - require.NoError(t, tc.Validate(output)) - }) - } -} - -func runCLICommand(t *testing.T, args ...string) string { - t.Helper() - - output, err := RunCommand(cliBinaryPath, args...) - require.NoError(t, err, "command %v failed: %s", args, output) - return output -} diff --git a/e2e/config.go b/e2e/config.go deleted file mode 100644 index b368305..0000000 --- a/e2e/config.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -// TestProxies contains the list of available test proxies -var TestProxies = []string{ - "http://34.146.222.111:8080", // Tokyo proxy 1 - "http://35.221.118.95:8080", // Tokyo proxy 2 - "http://34.142.205.26:8080", // Singapore proxy -} - -// GetDefaultProxy returns the first proxy as the default -func GetDefaultProxy() string { - return TestProxies[0] -} - -// GetProxySubset returns a subset of proxies for testing -func GetProxySubset(count int) []string { - if count >= len(TestProxies) { - return TestProxies - } - return TestProxies[:count] -} diff --git a/e2e/cross_node_test.go b/e2e/cross_node_test.go deleted file mode 100644 index 985a468..0000000 --- a/e2e/cross_node_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -// TestPublishWithoutSubscriptionOnDifferentNode tests that publishing fails -// when there's no subscriber on a different proxy node -func TestPublishWithoutSubscriptionOnDifferentNode(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - proxies := TestProxies - - testTopic := fmt.Sprintf("no-sub-cross-%d", time.Now().Unix()) - - tests := []struct { - name string - subscribeOn int // proxy index to subscribe on - publishOn int // proxy index to publish on - shouldFail bool - description string - }{ - { - name: "publish_without_any_subscription", - subscribeOn: -1, // No subscription - publishOn: 0, - shouldFail: true, - description: "Publishing without any subscriber should fail", - }, - { - name: "cross_proxy_same_region", - subscribeOn: 0, // Tokyo proxy 1 - publishOn: 1, // Tokyo proxy 2 - shouldFail: false, - description: "Publishing to different proxy in same region should work", - }, - { - name: "cross_proxy_different_region", - subscribeOn: 0, // Tokyo - publishOn: 2, // Singapore - shouldFail: false, - description: "Publishing across regions should work when subscriber exists", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var cancel context.CancelFunc - var subCmd *exec.Cmd - - // Start subscriber if needed - if tt.subscribeOn >= 0 { - ctx, c := context.WithCancel(context.Background()) - cancel = c - defer cancel() - - subProxy := proxies[tt.subscribeOn] - subCmd = exec.CommandContext(ctx, cliBinaryPath, - "subscribe", - "--topic="+testTopic, - "--service-url="+subProxy) - subCmd.Env = os.Environ() - err := subCmd.Start() - require.NoError(t, err, "Failed to start subscriber") - - // Wait for subscriber to be active - time.Sleep(3 * time.Second) - } - - // Attempt to publish - pubProxy := proxies[tt.publishOn] - - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - fmt.Sprintf("--message=CrossNodeTest-%s", tt.name), - "--service-url="+pubProxy) - - // Validate expectations - if tt.shouldFail { - // Should fail with "topic not assigned" or similar - require.Error(t, err, "Publishing without subscriber should have failed. Output: %s", out) - lowerOut := strings.ToLower(out) - if !strings.Contains(lowerOut, "topic not assigned") && - !strings.Contains(lowerOut, "not found") && - !strings.Contains(lowerOut, "failed") { - t.Logf("Unexpected error message: %s", out) - } - } else { - // Should succeed - require.NoError(t, err, "Publish failed: %v\nOutput: %s", err, out) - - validator := NewValidator(out) - err := validator.ValidatePublishSuccess() - require.NoError(t, err, "Publish validation failed") - } - - // Cleanup - if cancel != nil { - cancel() - if subCmd != nil { - subCmd.Wait() - } - } - - // Wait between tests to avoid rate limiting - time.Sleep(2 * time.Second) - }) - } -} - -// TestCrossProxyFailover tests behavior when one proxy is down -func TestCrossProxyFailover(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - testTopic := fmt.Sprintf("failover-%d", time.Now().Unix()) - - // Test with a definitely invalid proxy - invalidProxy := "http://192.0.2.1:8080" // TEST-NET-1 (non-routable) - - // Start subscriber on valid proxy first - validProxy := GetDefaultProxy() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, - "subscribe", - "--topic="+testTopic, - "--service-url="+validProxy) - subCmd.Env = os.Environ() - err := subCmd.Start() - require.NoError(t, err) - - time.Sleep(2 * time.Second) - - // Try publishing to invalid proxy (should fail) - start := time.Now() - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message=FailoverTest", - "--service-url="+invalidProxy) - duration := time.Since(start) - - // Should fail - require.Error(t, err, "Publishing to unreachable proxy should fail. Output: %s", out) - - // Should fail relatively quickly (not hang forever) - require.Less(t, duration.Seconds(), 35.0, - "Publish to unreachable proxy should timeout/fail within 35 seconds, took %v", duration) - - cancel() - subCmd.Wait() -} - -// TestMultipleSubscribersOnDifferentProxies tests message delivery to multiple subscribers -func TestMultipleSubscribersOnDifferentProxies(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - proxies := GetProxySubset(2) - - testTopic := fmt.Sprintf("multi-sub-%d", time.Now().Unix()) - testMessage := fmt.Sprintf("MultiSubTest-%d", time.Now().Unix()) - - // Start subscribers on both proxies - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - var subCmds []*exec.Cmd - for i, proxy := range proxies { - subCmd := exec.CommandContext(ctx, cliBinaryPath, - "subscribe", - "--topic="+testTopic, - "--service-url="+proxy) - subCmd.Env = os.Environ() - err := subCmd.Start() - require.NoError(t, err, "Failed to start subscriber %d on %s", i, proxy) - subCmds = append(subCmds, subCmd) - } - - // Wait for all subscribers to be ready - time.Sleep(4 * time.Second) - - // Publish message to first proxy - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message="+testMessage, - "--service-url="+proxies[0]) - - require.NoError(t, err, "Publish failed: %v\nOutput: %s", err, out) - - validator := NewValidator(out) - err = validator.ValidatePublishSuccess() - require.NoError(t, err) - - // Cleanup - cancel() - for _, cmd := range subCmds { - cmd.Wait() - } -} - -// TestProxyHealthBeforePublish tests that checking health before publishing is reliable -func TestProxyHealthBeforePublish(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - proxies := TestProxies - - for i, proxy := range proxies { - t.Run(fmt.Sprintf("proxy_%d", i+1), func(t *testing.T) { - // Check health first - healthOut, healthErr := RunCommand(cliBinaryPath, "health", "--service-url="+proxy) - - if healthErr != nil { - t.Skipf("Proxy %s is unhealthy, skipping: %v", proxy, healthErr) - return - } - - validator := NewValidator(healthOut) - healthInfo, err := validator.ValidateHealthCheck() - require.NoError(t, err, "Health check validation failed") - require.Equal(t, "ok", healthInfo.Status, "Proxy %s should be healthy", proxy) - - // If healthy, test should be able to publish (with subscriber) - testTopic := fmt.Sprintf("health-pub-%d-%d", i, time.Now().Unix()) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, - "subscribe", - "--topic="+testTopic, - "--service-url="+proxy) - subCmd.Env = os.Environ() - err = subCmd.Start() - require.NoError(t, err) - - time.Sleep(2 * time.Second) - - pubOut, pubErr := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message=HealthTest", - "--service-url="+proxy) - - cancel() - subCmd.Wait() - - require.NoError(t, pubErr, "Proxy reported healthy but publish failed: %s", pubOut) - }) - } -} diff --git a/e2e/failure_test.go b/e2e/failure_test.go deleted file mode 100644 index 0f0e0da..0000000 --- a/e2e/failure_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestFailureScenarios(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - tests := []struct { - name string - args []string - }{ - // Invalid commands - {"invalid command", []string{"invalid-command"}}, - {"unknown subcommand", []string{"foobar"}}, - - // Unknown flags - {"health unknown flag", []string{"health", "--invalid-flag"}}, - {"publish unknown flag", []string{"publish", "--invalid-flag"}}, - {"subscribe unknown flag", []string{"subscribe", "--unknown"}}, - - // Missing required flags - {"publish no topic", []string{"publish", "--message=test"}}, - {"publish no message", []string{"publish", "--topic=test"}}, - {"subscribe no topic", []string{"subscribe"}}, - - // Invalid flag values - {"invalid service-url format", []string{"health", "--service-url=not-a-url"}}, - {"empty topic name", []string{"publish", "--topic=", "--message=test"}}, - {"empty message", []string{"publish", "--topic=test", "--message="}}, - - // Invalid combinations - {"publish file and message both", []string{"publish", "--topic=test", "--message=test", "--file=test.txt"}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, tt.args...) - require.Error(t, err, "Expected command to fail but it succeeded. Output: %s", out) - }) - } -} - -func TestInvalidFlagValues(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - tests := []struct { - name string - args []string - }{ - // Service URL issues - {"malformed URL", []string{"health", "--service-url=://broken"}}, - {"missing protocol", []string{"health", "--service-url=localhost:8080"}}, - - // Numeric validation - {"negative port", []string{"health", "--service-url=http://localhost:-8080"}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, tt.args...) - require.Error(t, err, "Expected command to fail but it succeeded. Output: %s", out) - }) - } -} diff --git a/e2e/fuzz_test.go b/e2e/fuzz_test.go deleted file mode 100644 index 384c18f..0000000 --- a/e2e/fuzz_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package main - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -// FuzzPublishTopicName tests the publish command with a topic name -func FuzzPublishTopicName(f *testing.F) { - require.NotEmpty(f, cliBinaryPath, "CLI binary path must be set by TestMain") - - f.Add("") - f.Add("\x00") - f.Add("../../../etc/passwd") - f.Add(strings.Repeat("a", 1000)) - f.Add("test\n\r\t") - f.Add("test'; DROP TABLE") - f.Add("${HOME}") - - f.Fuzz(func(t *testing.T, topic string) { - if len(topic) > 5000 { - t.Skip() - } - - // For obviously invalid input, verify graceful error handling - if strings.Contains(topic, "\x00") { - args := []string{"publish", "--topic=" + topic, "--message=test"} - out, err := RunCommand(cliBinaryPath, args...) - require.Error(t, err, "CLI should reject null bytes in topic name") - // Verify error is handled gracefully (not a panic) - if strings.Contains(out, "panic") || strings.Contains(out, "fatal") { - t.Fatalf("CLI panicked or crashed on topic with null byte: %v\nOutput: %s", err, out) - } - return - } - - args := []string{"publish", "--topic=" + topic, "--message=test"} - out, err := RunCommand(cliBinaryPath, args...) - // For fuzzing, we need to detect failures - invalid topics should fail gracefully - // Valid topics might succeed (if subscribed) or fail (if not subscribed) - // The key is that the CLI should handle the input without panicking - if err != nil { - // Any error should be handled gracefully (not a panic or crash) - if strings.Contains(out, "panic") || strings.Contains(out, "fatal") { - t.Fatalf("CLI panicked or crashed on topic %q: %v\nOutput: %s", topic, err, out) - } - // For invalid topics, errors are expected and acceptable - t.Logf("Command failed (expected for invalid input): %v\nOutput: %s", err, out) - } - }) -} - -// FuzzPublishMessage tests the publish command with a message. -// This is distinct from FuzzPublishTopicName which tests topic name validation; -// this test focuses on message content validation and handling. -func FuzzPublishMessage(f *testing.F) { - require.NotEmpty(f, cliBinaryPath, "CLI binary path must be set by TestMain") - - f.Add("") - f.Add("\x00") - f.Add(strings.Repeat("a", 10000)) - f.Add("{\"test\": \"value\"}") - f.Add("test") - - f.Fuzz(func(t *testing.T, message string) { - if len(message) > 50000 { - t.Skip() - } - - // For obviously invalid input, verify graceful error handling - if strings.Contains(message, "\x00") { - args := []string{"publish", "--topic=fuzz-test", "--message=" + message} - out, err := RunCommand(cliBinaryPath, args...) - require.Error(t, err, "CLI should reject null bytes in message") - // Verify error is handled gracefully (not a panic) - if strings.Contains(out, "panic") || strings.Contains(out, "fatal") { - t.Fatalf("CLI panicked or crashed on message with null byte: %v\nOutput: %s", err, out) - } - return - } - - args := []string{"publish", "--topic=fuzz-test", "--message=" + message} - out, err := RunCommand(cliBinaryPath, args...) - // For fuzzing, we need to detect failures - invalid messages should fail gracefully - // Valid messages might succeed (if topic is subscribed) or fail (if not subscribed) - // The key is that the CLI should handle the input without panicking - if err != nil { - // Any error should be handled gracefully (not a panic or crash) - if strings.Contains(out, "panic") || strings.Contains(out, "fatal") { - t.Fatalf("CLI panicked or crashed on message %q: %v\nOutput: %s", message, err, out) - } - // For invalid messages, errors are expected and acceptable - t.Logf("Command failed (expected for invalid input): %v\nOutput: %s", err, out) - } - }) -} - -// FuzzServiceURL tests the health command with a service URL -func FuzzServiceURL(f *testing.F) { - require.NotEmpty(f, cliBinaryPath, "CLI binary path must be set by TestMain") - - f.Add("not-a-url") - f.Add("://broken") - f.Add("http://") - f.Add("http://localhost:-8080") - f.Add("http://localhost:99999") - f.Add("javascript:alert(1)") - f.Add("\x00") - - f.Fuzz(func(t *testing.T, url string) { - if len(url) > 1000 { - t.Skip() - } - - // For obviously invalid input, verify graceful error handling - if strings.Contains(url, "\x00") { - args := []string{"health", "--service-url=" + url} - out, err := RunCommand(cliBinaryPath, args...) - require.Error(t, err, "CLI should reject null bytes in service URL") - // Verify error is handled gracefully (not a panic) - if strings.Contains(out, "panic") || strings.Contains(out, "fatal") { - t.Fatalf("CLI panicked or crashed on service URL with null byte: %v\nOutput: %s", err, out) - } - return - } - - args := []string{"health", "--service-url=" + url} - out, err := RunCommand(cliBinaryPath, args...) - // For fuzzing, we need to detect failures - invalid URLs should fail gracefully - // Valid URLs might succeed (if proxy is reachable) or fail (if not) - // The key is that the CLI should handle the input without panicking - if err != nil { - // Any error should be handled gracefully (not a panic or crash) - if strings.Contains(out, "panic") || strings.Contains(out, "fatal") { - t.Fatalf("CLI panicked or crashed on URL %q: %v\nOutput: %s", url, err, out) - } - // For invalid URLs, errors are expected and acceptable - t.Logf("Command failed (expected for invalid URL): %v\nOutput: %s", err, out) - } - }) -} - -// FuzzFilePath tests the publish command with a file path -func FuzzFilePath(f *testing.F) { - require.NotEmpty(f, cliBinaryPath, "CLI binary path must be set by TestMain") - - f.Add("") - f.Add("nonexistent.txt") - f.Add("../../../etc/passwd") - f.Add("/dev/null") - f.Add("test\x00file.txt") - f.Add(strings.Repeat("a", 500) + ".txt") - - f.Fuzz(func(t *testing.T, filepath string) { - if len(filepath) > 2000 { - t.Skip() - } - - // For obviously invalid input, verify graceful error handling - if strings.Contains(filepath, "\x00") { - args := []string{"publish", "--topic=fuzz-test", "--file=" + filepath} - out, err := RunCommand(cliBinaryPath, args...) - require.Error(t, err, "CLI should reject null bytes in file path") - // Verify error is handled gracefully (not a panic) - if strings.Contains(out, "panic") || strings.Contains(out, "fatal") { - t.Fatalf("CLI panicked or crashed on file path with null byte: %v\nOutput: %s", err, out) - } - return - } - - args := []string{"publish", "--topic=fuzz-test", "--file=" + filepath} - out, err := RunCommand(cliBinaryPath, args...) - // For fuzzing, we need to detect failures - invalid file paths should fail gracefully - // Valid file paths might succeed (if file exists and topic is subscribed) or fail (if not) - // The key is that the CLI should handle the input without panicking - if err != nil { - // Any error should be handled gracefully (not a panic or crash) - if strings.Contains(out, "panic") || strings.Contains(out, "fatal") { - t.Fatalf("CLI panicked or crashed on file path %q: %v\nOutput: %s", filepath, err, out) - } - // For invalid file paths, errors are expected and acceptable - t.Logf("Command failed (expected for invalid file path): %v\nOutput: %s", err, out) - } - }) -} diff --git a/e2e/integration_test.go b/e2e/integration_test.go deleted file mode 100644 index f10ed21..0000000 --- a/e2e/integration_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestFullWorkflow(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - serviceURL := GetDefaultProxy() - - testTopic := fmt.Sprintf("workflow-%d", time.Now().Unix()) - - t.Run("1_check_version", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "version") - require.NoError(t, err, "version command failed: %v\nOutput: %s", err, out) - require.Contains(t, out, "Version:", "Expected version output") - require.Contains(t, out, "Commit:", "Expected commit hash") - }) - - t.Run("2_check_authentication", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "whoami") - require.NoError(t, err, "whoami command failed: %v\nOutput: %s", err, out) - require.Contains(t, out, "Authentication Status:", "Expected auth status") - require.Contains(t, out, "Client ID:", "Expected client ID") - }) - - t.Run("3_check_proxy_health", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "health", "--service-url="+serviceURL) - require.NoError(t, err, "health command failed: %v\nOutput: %s", err, out) - require.Contains(t, out, "Proxy Health Status:", "Expected health status") - }) - - // Start subscriber in background before publishing - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, "subscribe", "--topic="+testTopic, "--service-url="+serviceURL) - subCmd.Env = os.Environ() - err := subCmd.Start() - require.NoError(t, err, "Failed to start background subscriber") - - // Wait for subscription to be active - time.Sleep(2 * time.Second) - - t.Run("4_publish_http_message", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message=Integration test message", - "--service-url="+serviceURL) - require.NoError(t, err, "HTTP publish failed: %v\nOutput: %s", err, out) - require.Contains(t, strings.ToLower(out), "published", "Expected published confirmation") - }) - - t.Run("5_publish_grpc_message", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message=Integration gRPC test", - "--grpc", - "--service-url="+serviceURL) - require.NoError(t, err, "gRPC publish failed: %v\nOutput: %s", err, out) - require.Contains(t, strings.ToLower(out), "published", "Expected published confirmation") - }) - - t.Run("6_check_usage_stats", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "usage") - require.NoError(t, err, "usage command failed: %v\nOutput: %s", err, out) - require.Contains(t, out, "Publish (hour):", "Expected usage stats") - require.Contains(t, out, "Data Used:", "Expected data usage") - }) - - t.Run("7_list_topics", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "list-topics", "--service-url="+serviceURL) - require.NoError(t, err, "list-topics failed: %v\nOutput: %s", err, out) - require.Contains(t, out, "Subscribed Topics", "Expected topics list") - require.Contains(t, out, "Client:", "Expected client info") - }) - - t.Run("8_debug_mode_publish", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "--debug", "publish", - "--topic="+testTopic, - "--message=Debug workflow test", - "--service-url="+serviceURL) - require.NoError(t, err, "Debug publish failed: %v\nOutput: %s", err, out) - require.Contains(t, strings.ToLower(out), "publish:", "Expected debug output") - require.Contains(t, strings.ToLower(out), "sender_info:", "Expected sender info") - }) - - // Cleanup: stop subscriber - cancel() - subCmd.Wait() -} - -func TestCrossProxyWorkflow(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - // Test publishing and subscribing across different proxies - proxies := TestProxies - - testTopic := fmt.Sprintf("cross-proxy-%d", time.Now().Unix()) - - // Start subscriber on first proxy - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, "subscribe", "--topic="+testTopic, "--service-url="+proxies[0]) - subCmd.Env = os.Environ() - err := subCmd.Start() - require.NoError(t, err, "Failed to start background subscriber") - - // Wait for subscription to be active - time.Sleep(2 * time.Second) - - for i, proxy := range proxies { - proxyName := fmt.Sprintf("proxy_%d", i+1) - t.Run(proxyName+"_publish", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message=Cross-proxy test from "+proxyName, - "--service-url="+proxy) - require.NoError(t, err, "Publish to %s failed: %v\nOutput: %s", proxy, err, out) - require.Contains(t, strings.ToLower(out), "published", "Expected published confirmation") - }) - - t.Run(proxyName+"_health", func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, "health", "--service-url="+proxy) - require.NoError(t, err, "Health check for %s failed: %v\nOutput: %s", proxy, err, out) - require.Contains(t, out, "Proxy Health Status:", "Expected health status") - }) - } - - // Cleanup: stop subscriber - cancel() - subCmd.Wait() -} diff --git a/e2e/publish_test.go b/e2e/publish_test.go deleted file mode 100644 index bd84f32..0000000 --- a/e2e/publish_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestPublishCommand(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - serviceURL := GetDefaultProxy() - - testTopic := fmt.Sprintf("test-publish-%d", time.Now().Unix()) - - // Start a subscriber in the background to enable publishing - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, "subscribe", "--topic="+testTopic, "--service-url="+serviceURL) - subCmd.Env = os.Environ() - require.NoError(t, subCmd.Start(), "Failed to start background subscriber") - - // Wait for subscription to be active - time.Sleep(2 * time.Second) - - tests := []struct { - name string - args []string - expectError bool - expectOut []string - }{ - { - name: "publish HTTP inline message", - args: []string{"publish", "--topic=" + testTopic, "--message=Hello E2E Test", "--service-url=" + serviceURL}, - expectError: false, - expectOut: []string{"published", "topic"}, - }, - { - name: "publish gRPC inline message", - args: []string{"publish", "--topic=" + testTopic, "--message=Hello gRPC Test", "--grpc", "--service-url=" + serviceURL}, - expectError: false, - expectOut: []string{"published"}, - }, - { - name: "publish with debug mode HTTP", - args: []string{"--debug", "publish", "--topic=" + testTopic, "--message=Debug test", "--service-url=" + serviceURL}, - expectError: false, - expectOut: []string{"publish:", "sender_info:", "topic:"}, - }, - { - name: "publish with debug mode gRPC", - args: []string{"--debug", "publish", "--topic=" + testTopic, "--message=Debug gRPC", "--grpc", "--service-url=" + serviceURL}, - expectError: false, - expectOut: []string{"publish:", "sender_info:", "topic:"}, - }, - { - name: "publish missing topic flag", - args: []string{"publish", "--message=test"}, - expectError: true, - expectOut: []string{}, - }, - { - name: "publish missing message flag", - args: []string{"publish", "--topic=" + testTopic}, - expectError: true, - expectOut: []string{}, - }, - { - name: "publish with invalid service-url", - args: []string{"publish", "--topic=" + testTopic, "--message=test", "--service-url=invalid-url"}, - expectError: true, - expectOut: []string{}, - }, - } - - // Run the basic tests first - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - out, err := RunCommand(cliBinaryPath, tt.args...) - - if tt.expectError { - require.Error(t, err, "Expected command to fail but it succeeded. Output: %s", out) - } else { - require.NoError(t, err, "Command failed: %v\nOutput: %s", err, out) - - // Strict validation for publish success - validator := NewValidator(out) - err := validator.ValidatePublishSuccess() - require.NoError(t, err, "Publish validation failed: %v", err) - } - }) - } - - // Test --file flag scenarios - t.Run("publish from file HTTP", func(t *testing.T) { - dir := t.TempDir() - testFile := filepath.Join(dir, "test-publish.txt") - testContent := "Test file content for HTTP publish" - err := os.WriteFile(testFile, []byte(testContent), 0644) - require.NoError(t, err, "Failed to create test file") - - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--file="+testFile, - "--service-url="+serviceURL) - require.NoError(t, err, "File publish failed: %v\nOutput: %s", err, out) - - validator := NewValidator(out) - err = validator.ValidatePublishSuccess() - require.NoError(t, err, "File publish validation failed") - }) - - t.Run("publish from file gRPC", func(t *testing.T) { - dir := t.TempDir() - testFile := filepath.Join(dir, "test-publish-grpc.txt") - testContent := "Test file content for gRPC publish" - err := os.WriteFile(testFile, []byte(testContent), 0644) - require.NoError(t, err, "Failed to create test file") - - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--file="+testFile, - "--grpc", - "--service-url="+serviceURL) - require.NoError(t, err, "File gRPC publish failed: %v\nOutput: %s", err, out) - - validator := NewValidator(out) - err = validator.ValidatePublishSuccess() - require.NoError(t, err, "File gRPC publish validation failed") - }) - - t.Run("publish file not found", func(t *testing.T) { - dir := t.TempDir() - nonExistentFile := filepath.Join(dir, "nonexistent-file.txt") - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--file="+nonExistentFile, - "--service-url="+serviceURL) - require.Error(t, err, "Expected file not found error. Output: %s", out) - require.Contains(t, strings.ToLower(out), "failed to read file", "Expected file read error") - }) - - t.Run("publish file and message both (should fail)", func(t *testing.T) { - dir := t.TempDir() - testFile := filepath.Join(dir, "test-publish-both.txt") - err := os.WriteFile(testFile, []byte("test"), 0644) - require.NoError(t, err, "Failed to create test file") - - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--file="+testFile, - "--message=test", - "--service-url="+serviceURL) - require.Error(t, err, "Expected error when both --file and --message are provided. Output: %s", out) - require.Contains(t, strings.ToLower(out), "only one", "Expected error about using only one option") - }) - - // Cleanup: stop subscriber - cancel() - subCmd.Wait() -} diff --git a/e2e/ratelimit_scenarios_test.go b/e2e/ratelimit_scenarios_test.go deleted file mode 100644 index 112b2df..0000000 --- a/e2e/ratelimit_scenarios_test.go +++ /dev/null @@ -1,305 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -// getInitialPublishCount is a helper function to get the initial publish count from usage stats -func getInitialPublishCount(t *testing.T) int { - t.Helper() - usageBefore, err := RunCommand(cliBinaryPath, "usage") - require.NoError(t, err, "Failed to get initial usage stats") - - validatorBefore := NewValidator(usageBefore) - usageInfoBefore, err := validatorBefore.ValidateUsage() - require.NoError(t, err, "Failed to parse initial usage stats") - - return parsePublishCount(t, usageInfoBefore.PublishCount) -} - -// getMaxMessageSize gets the MaxMessageSize limit from whoami command output -func getMaxMessageSize(t *testing.T) int64 { - t.Helper() - whoamiOut, err := RunCommand(cliBinaryPath, "whoami") - require.NoError(t, err, "Failed to get whoami output") - - // Parse "Max Message Size: X.XX MB" from table format - // Format: "Max Message Size: 2.00 MB" - pattern := `Max Message Size:\s+([\d.]+)\s+MB` - validator := NewValidator(whoamiOut) - sizeMBStr, err := validator.ExtractMatch(pattern) - require.NoError(t, err, "Failed to extract Max Message Size from whoami output: %s", whoamiOut) - - sizeMB, err := strconv.ParseFloat(sizeMBStr, 64) - require.NoError(t, err, "Failed to parse Max Message Size as float: %s", sizeMBStr) - - // Convert MB to bytes - return int64(sizeMB * 1024 * 1024) -} - -// TestRateLimiterScenarios validates that usage stats change correctly after publishing messages -func TestRateLimiterScenarios(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - serviceURL := GetDefaultProxy() - testTopic := fmt.Sprintf("ratelimit-%d", time.Now().Unix()) - - beforeCount := getInitialPublishCount(t) - - // Start subscriber - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, "subscribe", "--topic="+testTopic, "--service-url="+serviceURL) - subCmd.Env = os.Environ() - err := subCmd.Start() - require.NoError(t, err, "Failed to start subscriber") - time.Sleep(2 * time.Second) - - // Publish multiple messages - numMessages := 3 - for i := 0; i < numMessages; i++ { - msg := fmt.Sprintf("RateLimitTest-%d", i+1) - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message="+msg, - "--service-url="+serviceURL) - require.NoError(t, err, "Publish %d failed: %s", i+1, out) - time.Sleep(500 * time.Millisecond) // Small delay between publishes - } - - cancel() - subCmd.Wait() - time.Sleep(1 * time.Second) - - // Get usage stats after publishing - usageAfter, err := RunCommand(cliBinaryPath, "usage") - require.NoError(t, err, "Failed to get usage stats after publishing") - - validatorAfter := NewValidator(usageAfter) - usageInfoAfter, err := validatorAfter.ValidateUsage() - require.NoError(t, err, "Failed to parse usage stats after publishing") - - afterCount := parsePublishCount(t, usageInfoAfter.PublishCount) - - // Verify publish count increased exactly by numMessages (tests not run in parallel) - require.Equal(t, beforeCount+numMessages, afterCount, - "Publish count should increase by exactly %d (before: %d, after: %d)", - numMessages, beforeCount, afterCount) - - // Verify data usage is present - require.Contains(t, usageAfter, "Data Used:", "Usage stats should show data usage") -} - -// TestRateLimitExceededPerHour tests that per-hour rate limit is enforced -func TestRateLimitExceededPerHour(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - serviceURL := GetDefaultProxy() - testTopic := fmt.Sprintf("ratelimit-hour-%d", time.Now().Unix()) - - // Get initial usage stats to determine per-hour limit - usageBefore, err := RunCommand(cliBinaryPath, "usage") - require.NoError(t, err, "Failed to get initial usage stats") - - validatorBefore := NewValidator(usageBefore) - usageInfoBefore, err := validatorBefore.ValidateUsage() - require.NoError(t, err, "Failed to parse initial usage stats") - - limitPerHour, err := strconv.Atoi(usageInfoBefore.PublishLimitPerHour) - require.NoError(t, err, "Failed to parse per-hour limit") - require.Greater(t, limitPerHour, 0, "Per-hour limit should be greater than 0") - - // Get current publish count - currentCount := parsePublishCount(t, usageInfoBefore.PublishCount) - - // Calculate how many more publishes we can do before hitting the limit - remaining := limitPerHour - currentCount - if remaining <= 0 { - t.Skipf("Already at or over per-hour limit (%d/%d). Cannot test limit enforcement.", currentCount, limitPerHour) - } - // Skip if remaining is too high to avoid long test times (e.g., > 100) - if remaining > 100 { - t.Skipf("Per-hour limit is too high (%d remaining). Skipping to avoid long test times.", remaining) - } - - // Start subscriber - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, "subscribe", "--topic="+testTopic, "--service-url="+serviceURL) - subCmd.Env = os.Environ() - err = subCmd.Start() - require.NoError(t, err, "Failed to start subscriber") - time.Sleep(2 * time.Second) - - // Publish up to the limit (should succeed) - for i := 0; i < remaining; i++ { - msg := fmt.Sprintf("RateLimitHourTest-%d", i+1) - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message="+msg, - "--service-url="+serviceURL) - require.NoError(t, err, "Publish %d should succeed: %s", i+1, out) - } - - // Try to publish one more (should exceed per-hour limit) - msg := fmt.Sprintf("RateLimitHourTest-%d", remaining+1) - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message="+msg, - "--service-url="+serviceURL) - require.Error(t, err, "Publish should fail when exceeding per-hour limit. Output: %s", out) - require.Contains(t, strings.ToLower(out), "per-hour", "Error should mention per-hour limit. Got: %s", out) - - cancel() - subCmd.Wait() -} - -// TestRateLimitExceededMessageSize tests that message size limit is enforced -func TestRateLimitExceededMessageSize(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - serviceURL := GetDefaultProxy() - testTopic := fmt.Sprintf("ratelimit-size-%d", time.Now().Unix()) - - // Start subscriber - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, "subscribe", "--topic="+testTopic, "--service-url="+serviceURL) - subCmd.Env = os.Environ() - err := subCmd.Start() - require.NoError(t, err, "Failed to start subscriber") - time.Sleep(2 * time.Second) - - // Get the actual MaxMessageSize limit from the token - maxMessageSize := getMaxMessageSize(t) - require.Greater(t, maxMessageSize, int64(0), "MaxMessageSize should be greater than 0") - - // Create a file with content that exceeds the limit by 1 byte - dir := t.TempDir() - largeFile := filepath.Join(dir, "large-message.txt") - largeContent := strings.Repeat("A", int(maxMessageSize)+1) // Exceed limit by 1 byte - err = os.WriteFile(largeFile, []byte(largeContent), 0644) - require.NoError(t, err, "Failed to create large test file") - - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--file="+largeFile, - "--service-url="+serviceURL) - require.Error(t, err, "Publish should fail when message size exceeds limit. Output: %s", out) - require.Contains(t, strings.ToLower(out), "message size", "Error should mention message size. Got: %s", out) - - cancel() - subCmd.Wait() -} - -// TestRateLimiterWithGRPC validates usage tracking with gRPC protocol -func TestRateLimiterWithGRPC(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - serviceURL := GetDefaultProxy() - testTopic := fmt.Sprintf("ratelimit-grpc-%d", time.Now().Unix()) - - beforeCount := getInitialPublishCount(t) - - // Start subscriber - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, "subscribe", "--topic="+testTopic, "--grpc", "--service-url="+serviceURL) - subCmd.Env = os.Environ() - err := subCmd.Start() - require.NoError(t, err, "Failed to start subscriber") - time.Sleep(2 * time.Second) - - // Publish via gRPC - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--message=RateLimitGRPCTest", - "--grpc", - "--service-url="+serviceURL) - require.NoError(t, err, "gRPC publish failed: %s", out) - - cancel() - subCmd.Wait() - time.Sleep(1 * time.Second) - - // Get usage stats after publishing - usageAfter, err := RunCommand(cliBinaryPath, "usage") - require.NoError(t, err, "Failed to get usage stats after publishing") - - validatorAfter := NewValidator(usageAfter) - usageInfoAfter, err := validatorAfter.ValidateUsage() - require.NoError(t, err, "Failed to parse usage stats after publishing") - - afterCount := parsePublishCount(t, usageInfoAfter.PublishCount) - - // Verify publish count increased exactly by 1 (tests not run in parallel) - require.Equal(t, beforeCount+1, afterCount, - "Publish count should increase by exactly 1 (before: %d, after: %d)", - beforeCount, afterCount) -} - -// TestRateLimiterWithFile validates usage tracking when publishing from file -func TestRateLimiterWithFile(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - serviceURL := GetDefaultProxy() - testTopic := fmt.Sprintf("ratelimit-file-%d", time.Now().Unix()) - - dir := t.TempDir() - testFile := filepath.Join(dir, "test-publish.txt") - testContent := "Test file content for rate limit tracking" - err := os.WriteFile(testFile, []byte(testContent), 0644) - require.NoError(t, err, "Failed to create test file") - - beforeCount := getInitialPublishCount(t) - - // Start subscriber - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, "subscribe", "--topic="+testTopic, "--service-url="+serviceURL) - subCmd.Env = os.Environ() - err = subCmd.Start() - require.NoError(t, err, "Failed to start subscriber") - time.Sleep(2 * time.Second) - - // Publish from file - out, err := RunCommand(cliBinaryPath, "publish", - "--topic="+testTopic, - "--file="+testFile, - "--service-url="+serviceURL) - require.NoError(t, err, "File publish failed: %s", out) - - cancel() - subCmd.Wait() - time.Sleep(1 * time.Second) - - // Get usage stats after publishing - usageAfter, err := RunCommand(cliBinaryPath, "usage") - require.NoError(t, err, "Failed to get usage stats after publishing") - - validatorAfter := NewValidator(usageAfter) - usageInfoAfter, err := validatorAfter.ValidateUsage() - require.NoError(t, err, "Failed to parse usage stats after publishing") - - afterCount := parsePublishCount(t, usageInfoAfter.PublishCount) - - // Verify publish count increased exactly by 1 (tests not run in parallel) - require.Equal(t, beforeCount+1, afterCount, - "Publish count should increase by exactly 1 (before: %d, after: %d)", - beforeCount, afterCount) -} diff --git a/e2e/ratelimit_test.go b/e2e/ratelimit_test.go deleted file mode 100644 index be8cdd3..0000000 --- a/e2e/ratelimit_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/exec" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -// TestDailyQuotaTracking validates that usage stats are properly tracked -func TestDailyQuotaTracking(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - usageBefore, err := RunCommand(cliBinaryPath, "usage") - require.NoError(t, err, "Failed to get usage stats") - - validator := NewValidator(usageBefore) - usageInfoBefore, err := validator.ValidateUsage() - require.NoError(t, err, "Failed to parse usage stats") - - serviceURL := GetDefaultProxy() - - testTopic := fmt.Sprintf("quota-%d", time.Now().Unix()) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - subCmd := exec.CommandContext(ctx, cliBinaryPath, "subscribe", "--topic="+testTopic, "--service-url="+serviceURL) - subCmd.Env = os.Environ() - err = subCmd.Start() - require.NoError(t, err) - time.Sleep(2 * time.Second) - - out, err := RunCommand(cliBinaryPath, "publish", "--topic="+testTopic, "--message=QuotaTrackingTest", "--service-url="+serviceURL) - require.NoError(t, err, "Publish failed: %s", out) - - cancel() - subCmd.Wait() - time.Sleep(1 * time.Second) - - usageAfter, err := RunCommand(cliBinaryPath, "usage") - require.NoError(t, err, "Failed to get usage stats after publish") - - validatorAfter := NewValidator(usageAfter) - usageInfoAfter, err := validatorAfter.ValidateUsage() - require.NoError(t, err, "Failed to parse usage stats after publish") - - // Verify usage increased - require.Contains(t, usageAfter, "Data Used:", "Usage stats should show data usage") - - // Parse publish counts to verify they increased exactly by 1 (tests not run in parallel) - beforeCount := parsePublishCount(t, usageInfoBefore.PublishCount) - afterCount := parsePublishCount(t, usageInfoAfter.PublishCount) - require.Equal(t, beforeCount+1, afterCount, - "Publish count should increase by exactly 1 (before: %d, after: %d)", - beforeCount, afterCount) -} - -// parsePublishCount parses the publish count string to an integer -func parsePublishCount(t *testing.T, countStr string) int { - t.Helper() - count, err := strconv.Atoi(countStr) - require.NoError(t, err, "Failed to parse publish count '%s'", countStr) - return count -} diff --git a/e2e/setup.go b/e2e/setup.go deleted file mode 100644 index 478ac3b..0000000 --- a/e2e/setup.go +++ /dev/null @@ -1,79 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "runtime" -) - -// PrepareCLI sets up the test environment and returns the CLI binary path -// Token setup is optional - if it fails, we continue without auth (useful for fuzz tests) -func PrepareCLI() (cliPath string, cleanup func(), err error) { - tokenPath, tokenErr := SetupTokenFile() - if tokenErr == nil { - // Token setup succeeded, set it up - if err := os.Setenv("MUMP2P_AUTH_PATH", tokenPath); err != nil { - return "", nil, fmt.Errorf("failed to set MUMP2P_AUTH_PATH: %w", err) - } - } - // If token setup failed, we continue without auth (for fuzz tests that don't need it) - repoRoot, err := findRepoRoot() - if err != nil { - return "", nil, err - } - - cli := os.Getenv("MUMP2P_E2E_CLI_BINARY") - if cli == "" { - osName := runtime.GOOS - if osName == "darwin" { - osName = "mac" - } - cli = filepath.Join(repoRoot, "dist", fmt.Sprintf("mump2p-%s", osName)) - } else if !filepath.IsAbs(cli) { - cli = filepath.Join(repoRoot, cli) - } - - if _, err := os.Stat(cli); errors.Is(err, os.ErrNotExist) { - return "", nil, fmt.Errorf("binary not found at %s\nRun 'make build' first with release credentials", cli) - } - - stat, err := os.Stat(cli) - if err != nil { - return "", nil, fmt.Errorf("failed to stat CLI binary: %w", err) - } - if stat.Mode()&0111 == 0 { - return "", nil, fmt.Errorf("binary %s is not executable", cli) - } - - cleanup = func() { - if tokenPath != "" { - _ = os.RemoveAll(filepath.Dir(tokenPath)) - } - } - - return cli, cleanup, nil -} - -func findRepoRoot() (string, error) { - wd, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to determine working directory: %w", err) - } - - dir := wd - for { - if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { - return dir, nil - } - - parent := filepath.Dir(dir) - if parent == dir { - break - } - dir = parent - } - - return "", fmt.Errorf("could not locate repo root from %s", wd) -} diff --git a/e2e/smoke_cases.go b/e2e/smoke_cases.go deleted file mode 100644 index c417735..0000000 --- a/e2e/smoke_cases.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "fmt" -) - -type cliCommandCase struct { - Name string - Args []string - StrictValidate func(string) error // New: strict validation function -} - -func (c cliCommandCase) Validate(output string) error { - if c.StrictValidate != nil { - return c.StrictValidate(output) - } - return nil -} - -func smokeTestCases() []cliCommandCase { - serviceURL := GetDefaultProxy() - - return []cliCommandCase{ - { - Name: "version", - Args: []string{"version"}, - StrictValidate: func(output string) error { - validator := NewValidator(output) - versionInfo, err := validator.ValidateVersion() - if err != nil { - return err - } - // Additional validation: version should not be empty - if versionInfo.Version == "" { - return fmt.Errorf("version is empty") - } - if versionInfo.Commit == "" { - return fmt.Errorf("commit hash is empty") - } - return nil - }, - }, - { - Name: "health", - Args: []string{"health", "--service-url=" + serviceURL}, - StrictValidate: func(output string) error { - validator := NewValidator(output) - healthInfo, err := validator.ValidateHealthCheck() - if err != nil { - return err - } - // Validate status is not empty - if healthInfo.Status == "" { - return fmt.Errorf("health status is empty") - } - return nil - }, - }, - { - Name: "whoami", - Args: []string{"whoami"}, - StrictValidate: func(output string) error { - validator := NewValidator(output) - whoamiInfo, err := validator.ValidateWhoami() - if err != nil { - return err - } - // Validate client ID is not empty - if whoamiInfo.ClientID == "" { - return fmt.Errorf("client ID is empty") - } - return nil - }, - }, - { - Name: "usage", - Args: []string{"usage"}, - StrictValidate: func(output string) error { - validator := NewValidator(output) - usageInfo, err := validator.ValidateUsage() - if err != nil { - return err - } - // Validate publish count exists (can be "0") - if usageInfo.PublishCount == "" { - return fmt.Errorf("publish count is empty") - } - return nil - }, - }, - { - Name: "list-topics", - Args: []string{"list-topics", "--service-url=" + serviceURL}, - StrictValidate: func(output string) error { - validator := NewValidator(output) - return validator.ContainsAll("Subscribed Topics", "Client:") - }, - }, - } -} diff --git a/e2e/subscribe_test.go b/e2e/subscribe_test.go deleted file mode 100644 index 1ba62d4..0000000 --- a/e2e/subscribe_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestSubscribeCommand(t *testing.T) { - require.NotEmpty(t, cliBinaryPath, "CLI binary path must be set by TestMain") - - serviceURL := GetDefaultProxy() - - // Add delay to allow P2P nodes to be ready - time.Sleep(3 * time.Second) - - testTopic := fmt.Sprintf("test-sub-%d", time.Now().Unix()) - - tests := []struct { - name string - args []string - expectError bool - expectOut []string - timeout time.Duration - }{ - { - name: "subscribe WebSocket", - args: []string{"subscribe", "--topic=" + testTopic, "--service-url=" + serviceURL}, - expectError: false, - expectOut: []string{"subscription", testTopic}, - timeout: 5 * time.Second, - }, - { - name: "subscribe gRPC", - args: []string{"subscribe", "--topic=" + testTopic, "--grpc", "--service-url=" + serviceURL}, - expectError: false, - expectOut: []string{"subscription", testTopic}, - timeout: 5 * time.Second, - }, - { - name: "subscribe with debug WebSocket", - args: []string{"--debug", "subscribe", "--topic=" + testTopic, "--service-url=" + serviceURL}, - expectError: false, - expectOut: []string{testTopic}, - timeout: 5 * time.Second, - }, - { - name: "subscribe with debug gRPC", - args: []string{"--debug", "subscribe", "--topic=" + testTopic, "--grpc", "--service-url=" + serviceURL}, - expectError: false, - expectOut: []string{testTopic}, - timeout: 5 * time.Second, - }, - { - name: "subscribe missing topic flag", - args: []string{"subscribe"}, - expectError: true, - expectOut: []string{}, - timeout: 1 * time.Second, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), tt.timeout) - defer cancel() - - cmd := exec.CommandContext(ctx, cliBinaryPath, tt.args...) - cmd.Env = os.Environ() - - output, err := cmd.CombinedOutput() - out := string(output) - - if tt.expectError { - require.Error(t, err, "Expected command to fail but it succeeded. Output: %s", out) - } else { - // For subscribe, context deadline exceeded is expected (we kill it after timeout) - if ctx.Err() == context.DeadlineExceeded { - // This is OK - we just wanted to test connection - for _, expected := range tt.expectOut { - require.Contains(t, strings.ToLower(out), strings.ToLower(expected), - "Expected output to contain %q, got %q", expected, out) - } - } else if err != nil { - t.Logf("Subscribe command ended early: %v\nOutput: %s", err, out) - // Still check if we got expected output before exit - for _, expected := range tt.expectOut { - require.Contains(t, strings.ToLower(out), strings.ToLower(expected), - "Expected output to contain %q, got %q", expected, out) - } - } - } - }) - } -} diff --git a/e2e/suite_test.go b/e2e/suite_test.go deleted file mode 100644 index 093e116..0000000 --- a/e2e/suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - "os" - "testing" -) - -var ( - cliBinaryPath string - cliCleanup func() -) - -func TestMain(m *testing.M) { - var err error - cliBinaryPath, cliCleanup, err = PrepareCLI() - if err != nil { - fmt.Fprintf(os.Stderr, "[e2e] failed to prepare CLI: %v\n", err) - os.Exit(1) - } - - code := m.Run() - if cliCleanup != nil { - cliCleanup() - } - os.Exit(code) -} diff --git a/e2e/token.go b/e2e/token.go deleted file mode 100644 index a66e33f..0000000 --- a/e2e/token.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "encoding/base64" - "fmt" - "os" - "path/filepath" - "strings" -) - -// SetupTokenFile creates a temporary auth file for testing -// Uses MUMP2P_E2E_TOKEN_B64 env var (CI) or falls back to ~/.mump2p/auth.yml (local) -func SetupTokenFile() (string, error) { - if b64 := strings.TrimSpace(os.Getenv("MUMP2P_E2E_TOKEN_B64")); b64 != "" { - decoded, err := base64.StdEncoding.DecodeString(b64) - if err != nil { - return "", fmt.Errorf("failed to decode MUMP2P_E2E_TOKEN_B64: %w", err) - } - return writeTokenFile(decoded) - } - - homeDir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get home directory: %w", err) - } - - localAuthPath := filepath.Join(homeDir, ".mump2p", "auth.yml") - data, err := os.ReadFile(localAuthPath) - if err == nil { - return writeTokenFile(data) - } - - return "", fmt.Errorf("no token available: set MUMP2P_E2E_TOKEN_B64 or login with ./mump2p login") -} - -func writeTokenFile(content []byte) (string, error) { - tmpDir, err := os.MkdirTemp("", "token") - if err != nil { - return "", err - } - - tmpFile := filepath.Join(tmpDir, "auth.yml") - if err := os.WriteFile(tmpFile, content, 0600); err != nil { - return "", err - } - - return tmpFile, nil -} diff --git a/e2e/validators.go b/e2e/validators.go deleted file mode 100644 index 00983dc..0000000 --- a/e2e/validators.go +++ /dev/null @@ -1,213 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "regexp" - "strings" -) - -// OutputValidator provides strict validation for CLI output -type OutputValidator struct { - output string -} - -// NewValidator creates a new output validator -func NewValidator(output string) *OutputValidator { - return &OutputValidator{output: output} -} - -// ContainsAll validates that output contains all expected strings -func (v *OutputValidator) ContainsAll(expected ...string) error { - for _, exp := range expected { - if !strings.Contains(v.output, exp) { - return fmt.Errorf("expected output to contain %q, got:\n%s", exp, v.output) - } - } - return nil -} - -// MatchesPattern validates output against a regex pattern -func (v *OutputValidator) MatchesPattern(pattern string) error { - re, err := regexp.Compile(pattern) - if err != nil { - return fmt.Errorf("invalid regex pattern: %w", err) - } - if !re.MatchString(v.output) { - return fmt.Errorf("output does not match pattern %q, got:\n%s", pattern, v.output) - } - return nil -} - -// ExtractMatch extracts the first match of a regex pattern -func (v *OutputValidator) ExtractMatch(pattern string) (string, error) { - re, err := regexp.Compile(pattern) - if err != nil { - return "", fmt.Errorf("invalid regex pattern: %w", err) - } - matches := re.FindStringSubmatch(v.output) - if len(matches) < 2 { - return "", fmt.Errorf("no match found for pattern %q in output:\n%s", pattern, v.output) - } - return matches[1], nil -} - -// ValidateJSON checks if output is valid JSON and matches structure -func (v *OutputValidator) ValidateJSON(target interface{}) error { - if err := json.Unmarshal([]byte(v.output), target); err != nil { - return fmt.Errorf("failed to parse JSON: %w\nOutput:\n%s", err, v.output) - } - return nil -} - -// NotContains validates that output does NOT contain certain strings -func (v *OutputValidator) NotContains(unwanted ...string) error { - for _, unw := range unwanted { - if strings.Contains(v.output, unw) { - return fmt.Errorf("output should not contain %q, but got:\n%s", unw, v.output) - } - } - return nil -} - -// ValidateVersion checks version output format (e.g., "Version: v0.0.1-rc7\nCommit: 8e333bf") -func (v *OutputValidator) ValidateVersion() (*VersionInfo, error) { - info := &VersionInfo{} - - versionPattern := `Version:\s+(v?\d+\.\d+\.\d+(?:-[\w.]+)?)` - version, err := v.ExtractMatch(versionPattern) - if err != nil { - return nil, fmt.Errorf("invalid version format: %w", err) - } - info.Version = version - - commitPattern := `Commit:\s+([a-f0-9]{7,40})` - commit, err := v.ExtractMatch(commitPattern) - if err != nil { - return nil, fmt.Errorf("invalid commit format: %w", err) - } - info.Commit = commit - - return info, nil -} - -// ValidateWhoami checks whoami output format -func (v *OutputValidator) ValidateWhoami() (*WhoamiInfo, error) { - info := &WhoamiInfo{} - - // Check for authentication status - if err := v.ContainsAll("Authentication Status:", "Client ID:"); err != nil { - return nil, err - } - - // Extract client ID (Auth0 format or email) - clientPattern := `Client ID:\s+(.+?)(?:\n|$)` - clientID, err := v.ExtractMatch(clientPattern) - if err != nil { - return nil, fmt.Errorf("could not extract client ID: %w", err) - } - info.ClientID = strings.TrimSpace(clientID) - - if info.ClientID == "" { - return nil, fmt.Errorf("client ID is empty") - } - - return info, nil -} - -// ValidatePublishSuccess checks successful publish output -func (v *OutputValidator) ValidatePublishSuccess() error { - // Must contain published confirmation - publishedPattern := `(?i)(published|message sent successfully)` - if err := v.MatchesPattern(publishedPattern); err != nil { - return fmt.Errorf("publish success not confirmed: %w", err) - } - - // Should not contain error keywords - if err := v.NotContains("Error:", "error:", "failed", "Failed"); err != nil { - return err - } - - return nil -} - -// ValidateHealthCheck validates health check output -func (v *OutputValidator) ValidateHealthCheck() (*HealthInfo, error) { - info := &HealthInfo{} - - if err := v.ContainsAll("Proxy Health Status:"); err != nil { - return nil, err - } - - // Extract status (ok, unhealthy, etc.) - format: "Status: ok" - statusPattern := `Status:\s+(\w+)` - status, err := v.ExtractMatch(statusPattern) - if err != nil { - return nil, fmt.Errorf("could not extract health status: %w", err) - } - info.Status = strings.TrimSpace(status) - - return info, nil -} - -// ValidateUsage validates usage stats output -func (v *OutputValidator) ValidateUsage() (*UsageInfo, error) { - info := &UsageInfo{} - - if err := v.ContainsAll("Publish (hour):", "Data Used:"); err != nil { - return nil, err - } - - // Extract publish count and limit (format: "Publish (hour): 5 / 100") - publishPattern := `Publish \(hour\):\s+(\d+)\s+/\s+(\d+)` - matches := regexp.MustCompile(publishPattern).FindStringSubmatch(v.output) - if len(matches) < 3 { - return nil, fmt.Errorf("could not extract publish count and limit") - } - info.PublishCount = matches[1] - info.PublishLimitPerHour = matches[2] - - // Extract per-second count and limit (format: "Publish (second): 1 / 2") - secondPattern := `Publish \(second\):\s+(\d+)\s+/\s+(\d+)` - secondMatches := regexp.MustCompile(secondPattern).FindStringSubmatch(v.output) - if len(secondMatches) >= 3 { - info.SecondPublishCount = secondMatches[1] - info.PublishLimitPerSec = secondMatches[2] - } - - // Extract data used and daily quota (format: "Data Used: 0.0000 MB / 100.0000 MB") - dataPattern := `Data Used:\s+([\d.]+)\s+MB\s+/\s+([\d.]+)\s+MB` - dataMatches := regexp.MustCompile(dataPattern).FindStringSubmatch(v.output) - if len(dataMatches) >= 3 { - info.BytesPublishedMB = dataMatches[1] - info.DailyQuotaMB = dataMatches[2] - } - - return info, nil -} - -// VersionInfo holds parsed version information -type VersionInfo struct { - Version string - Commit string -} - -// WhoamiInfo holds parsed whoami information -type WhoamiInfo struct { - ClientID string -} - -// HealthInfo holds parsed health check information -type HealthInfo struct { - Status string -} - -// UsageInfo holds parsed usage statistics -type UsageInfo struct { - PublishCount string - PublishLimitPerHour string - SecondPublishCount string - PublishLimitPerSec string - BytesPublishedMB string - DailyQuotaMB string -} diff --git a/go.mod b/go.mod index 86f548b..314823d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.24.6 require ( github.com/gizak/termui/v3 v3.1.0 github.com/golang-jwt/jwt/v4 v4.5.2 - github.com/gorilla/websocket v1.5.3 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.11.1 google.golang.org/grpc v1.73.0 diff --git a/go.sum b/go.sum index 7e5f619..506cdf2 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= diff --git a/internal/grpc/proxy_client.go b/internal/grpc/proxy_client.go deleted file mode 100644 index d4e5fdb..0000000 --- a/internal/grpc/proxy_client.go +++ /dev/null @@ -1,128 +0,0 @@ -package grpc - -import ( - "context" - "fmt" - "io" - "math" - - proto "github.com/getoptimum/mump2p-cli/proto" - grpcClient "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - -// ProxyClient handles gRPC streaming connections to the proxy -type ProxyClient struct { - conn *grpcClient.ClientConn - client proto.ProxyStreamClient -} - -// NewProxyClient creates a new gRPC proxy client -func NewProxyClient(proxyAddr string) (*ProxyClient, error) { - conn, err := grpcClient.NewClient(proxyAddr, - grpcClient.WithTransportCredentials(insecure.NewCredentials()), - grpcClient.WithDefaultCallOptions( - grpcClient.MaxCallRecvMsgSize(math.MaxInt), - grpcClient.MaxCallSendMsgSize(math.MaxInt), - ), - ) - if err != nil { - return nil, fmt.Errorf("failed to connect to proxy: %v", err) - } - - client := proto.NewProxyStreamClient(conn) - return &ProxyClient{ - conn: conn, - client: client, - }, nil -} - -// Subscribe starts a gRPC stream subscription and returns a channel for receiving messages -func (pc *ProxyClient) Subscribe(ctx context.Context, clientID string, bufferSize int) (<-chan *proto.ProxyMessage, error) { - stream, err := pc.client.ClientStream(ctx) - if err != nil { - return nil, fmt.Errorf("failed to open stream: %v", err) - } - - // Send initial client ID message - if err := stream.Send(&proto.ProxyMessage{ - ClientId: clientID, - Type: "subscribe", - }); err != nil { - return nil, fmt.Errorf("failed to send client ID: %v", err) - } - - // Create channel for receiving messages - msgChan := make(chan *proto.ProxyMessage, bufferSize) - - // Start goroutine to receive messages - go func() { - defer close(msgChan) - defer func() { _ = stream.CloseSend() }() - - for { - msg, err := stream.Recv() - if err == io.EOF { - return - } - if err != nil { - // Log error but don't close channel immediately to allow reconnection - fmt.Printf("Stream receive error: %v\n", err) - return - } - - select { - case msgChan <- msg: - case <-ctx.Done(): - return - } - } - }() - - return msgChan, nil -} - -// Close closes the gRPC connection -func (pc *ProxyClient) Close() error { - return pc.conn.Close() -} - -// SubscribeTopic sends a gRPC subscription request to register for a topic -func (pc *ProxyClient) SubscribeTopic(ctx context.Context, clientID, topic string, threshold float32) error { - req := &proto.SubscribeRequest{ - ClientId: clientID, - Topic: topic, - Threshold: threshold, - } - - resp, err := pc.client.Subscribe(ctx, req) - if err != nil { - return fmt.Errorf("gRPC subscribe failed: %v", err) - } - - if resp.Status != "subscribed" { - return fmt.Errorf("subscribe failed with status: %s", resp.Status) - } - - return nil -} - -// Publish sends a message to a topic via gRPC -func (pc *ProxyClient) Publish(ctx context.Context, clientID, topic string, message []byte) error { - req := &proto.PublishRequest{ - ClientId: clientID, - Topic: topic, - Message: message, - } - - resp, err := pc.client.Publish(ctx, req) - if err != nil { - return fmt.Errorf("gRPC publish failed: %v", err) - } - - if resp.Status != "published" { - return fmt.Errorf("publish failed with status: %s", resp.Status) - } - - return nil -} diff --git a/internal/node/client.go b/internal/node/client.go new file mode 100644 index 0000000..e663c39 --- /dev/null +++ b/internal/node/client.go @@ -0,0 +1,118 @@ +package node + +import ( + "context" + "fmt" + "io" + "math" + + pb "github.com/getoptimum/mump2p-cli/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +const ( + CommandPublishData int32 = 1 + CommandSubscribeToTopic int32 = 2 + CommandSubscribeToTopics int32 = 4 +) + +type Client struct { + conn *grpc.ClientConn + client pb.CommandStreamClient +} + +func NewClient(nodeAddr string) (*Client, error) { + conn, err := grpc.NewClient(nodeAddr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(math.MaxInt), + grpc.MaxCallSendMsgSize(math.MaxInt), + ), + ) + if err != nil { + return nil, fmt.Errorf("failed to connect to node %s: %w", nodeAddr, err) + } + return &Client{ + conn: conn, + client: pb.NewCommandStreamClient(conn), + }, nil +} + +// Subscribe opens a bidi stream, sends a subscribe command, and returns a +// channel that delivers raw message payloads received from the mesh. +func (c *Client) Subscribe(ctx context.Context, ticket, topic string, bufSize int) (<-chan *pb.Response, error) { + stream, err := c.client.ListenCommands(ctx) + if err != nil { + return nil, fmt.Errorf("failed to open command stream: %w", err) + } + + if err := stream.Send(&pb.Request{ + Command: CommandSubscribeToTopic, + Topic: topic, + JwtToken: ticket, + }); err != nil { + return nil, fmt.Errorf("failed to send subscribe command: %w", err) + } + + ch := make(chan *pb.Response, bufSize) + go func() { + defer close(ch) + for { + resp, err := stream.Recv() + if err == io.EOF { + return + } + if err != nil { + select { + case <-ctx.Done(): + default: + fmt.Printf("stream error: %v\n", err) + } + return + } + select { + case ch <- resp: + case <-ctx.Done(): + return + } + } + }() + + return ch, nil +} + +// Publish opens a bidi stream, sends a publish command, and reads back the +// first response (typically a MessageTrace confirmation). +func (c *Client) Publish(ctx context.Context, ticket, topic string, data []byte) (*pb.Response, error) { + stream, err := c.client.ListenCommands(ctx) + if err != nil { + return nil, fmt.Errorf("failed to open command stream: %w", err) + } + + if err := stream.Send(&pb.Request{ + Command: CommandPublishData, + Topic: topic, + Data: data, + JwtToken: ticket, + }); err != nil { + return nil, fmt.Errorf("failed to send publish command: %w", err) + } + + if err := stream.CloseSend(); err != nil { + return nil, fmt.Errorf("failed to close send: %w", err) + } + + resp, err := stream.Recv() + if err == io.EOF { + return nil, fmt.Errorf("node closed stream before sending publish response (EOF)") + } + if err != nil { + return nil, fmt.Errorf("failed to receive publish response: %w", err) + } + return resp, nil +} + +func (c *Client) Close() error { + return c.conn.Close() +} diff --git a/internal/session/client.go b/internal/session/client.go new file mode 100644 index 0000000..1e78a80 --- /dev/null +++ b/internal/session/client.go @@ -0,0 +1,89 @@ +package session + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type Node struct { + ID string `json:"id"` + Address string `json:"address"` + Transport string `json:"transport"` + Region string `json:"region"` + Ticket string `json:"ticket"` + Score float32 `json:"score"` +} + +type Session struct { + SessionID string `json:"session_id"` + Nodes []Node `json:"nodes"` + ExpiresAt string `json:"expires_at"` + RefreshAfter string `json:"refresh_after"` + Error string `json:"error,omitempty"` +} + +type sessionRequest struct { + ClientID string `json:"client_id"` + Topics []string `json:"topics"` + Capabilities []string `json:"capabilities"` + ExposeAmount uint32 `json:"expose_amount"` +} + +func CreateSession(proxyURL, clientID, accessToken string, topics, capabilities []string, exposeAmount uint32) (*Session, error) { + reqData := sessionRequest{ + ClientID: clientID, + Topics: topics, + Capabilities: capabilities, + ExposeAmount: exposeAmount, + } + + body, err := json.Marshal(reqData) + if err != nil { + return nil, fmt.Errorf("failed to marshal session request: %w", err) + } + + url := proxyURL + "/api/v1/session" + httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + if accessToken != "" { + httpReq.Header.Set("Authorization", "Bearer "+accessToken) + } + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("session request failed: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("proxy returned status %d: %s", resp.StatusCode, string(respBody)) + } + + var sess Session + if err := json.Unmarshal(respBody, &sess); err != nil { + return nil, fmt.Errorf("failed to parse session response: %w", err) + } + + if sess.Error != "" { + return nil, fmt.Errorf("session error: %s", sess.Error) + } + + if len(sess.Nodes) == 0 { + return nil, fmt.Errorf("no nodes available") + } + + return &sess, nil +} diff --git a/internal/session/store.go b/internal/session/store.go new file mode 100644 index 0000000..23443d7 --- /dev/null +++ b/internal/session/store.go @@ -0,0 +1,206 @@ +package session + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "syscall" + "time" +) + +type CachedSession struct { + ProxyURL string `json:"proxy_url"` + ClientID string `json:"client_id"` + Topics []string `json:"topics"` + Capabilities []string `json:"capabilities"` + ExposeAmount uint32 `json:"expose_amount"` + Session Session `json:"session"` +} + +func sessionDir() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + dir := filepath.Join(home, ".mump2p") + if err := os.MkdirAll(dir, 0700); err != nil { + return "", err + } + return dir, nil +} + +func cachePath() (string, error) { + dir, err := sessionDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "session.json"), nil +} + +func lockPath() (string, error) { + dir, err := sessionDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "session.lock"), nil +} + +func acquireLock() (*os.File, error) { + p, err := lockPath() + if err != nil { + return nil, err + } + f, err := os.OpenFile(p, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return nil, err + } + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { + f.Close() + return nil, err + } + return f, nil +} + +func releaseLock(f *os.File) { + syscall.Flock(int(f.Fd()), syscall.LOCK_UN) //nolint:errcheck + f.Close() +} + +func loadCached() (*CachedSession, error) { + p, err := cachePath() + if err != nil { + return nil, err + } + data, err := os.ReadFile(p) + if err != nil { + return nil, err + } + var c CachedSession + if err := json.Unmarshal(data, &c); err != nil { + return nil, err + } + return &c, nil +} + +func saveCached(c *CachedSession) error { + p, err := cachePath() + if err != nil { + return err + } + data, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err + } + return os.WriteFile(p, data, 0600) +} + +func sortedKey(s []string) string { + cp := make([]string, len(s)) + copy(cp, s) + sort.Strings(cp) + return strings.Join(cp, ",") +} + +func (c *CachedSession) matches(proxyURL, clientID string, topics, capabilities []string) bool { + if c.ProxyURL != proxyURL || c.ClientID != clientID { + return false + } + if sortedKey(c.Capabilities) != sortedKey(capabilities) { + return false + } + cached := make(map[string]bool, len(c.Topics)) + for _, t := range c.Topics { + cached[t] = true + } + for _, t := range topics { + if !cached[t] { + return false + } + } + return true +} + +func (c *CachedSession) needsRefresh() bool { + ra, err := time.Parse(time.RFC3339, c.Session.RefreshAfter) + if err != nil { + return true + } + return time.Now().UTC().After(ra) +} + +func (c *CachedSession) isExpired() bool { + ea, err := time.Parse(time.RFC3339, c.Session.ExpiresAt) + if err != nil { + return true + } + return time.Now().UTC().After(ea) +} + +func isUsable(cached *CachedSession, proxyURL, clientID string, topics, capabilities []string, exposeAmount uint32) bool { + if cached == nil || + !cached.matches(proxyURL, clientID, topics, capabilities) || + cached.isExpired() || + cached.needsRefresh() { + return false + } + // Cached session must cover the requested node count (e.g. cannot reuse a + // 1-node session when the user now asks for --expose-amount 3). + if exposeAmount > 0 && len(cached.Session.Nodes) < int(exposeAmount) { + return false + } + return true +} + +// GetOrCreateSession returns a cached session if valid, refreshes if past +// the refresh window, or creates a new session. Uses a file lock to prevent +// concurrent processes from each creating separate sessions. +func GetOrCreateSession(proxyURL, clientID, accessToken string, topics, capabilities []string, exposeAmount uint32) (*Session, bool, error) { + // Fast path: read without lock — if valid, return immediately. + if cached, err := loadCached(); err == nil && isUsable(cached, proxyURL, clientID, topics, capabilities, exposeAmount) { + return &cached.Session, true, nil + } + + // Slow path: acquire lock, re-check (another process may have refreshed). + lf, lockErr := acquireLock() + if lockErr != nil { + // If locking fails, fall through to create without cache. + sess, err := CreateSession(proxyURL, clientID, accessToken, topics, capabilities, exposeAmount) + return sess, false, err + } + defer releaseLock(lf) + + if cached, err := loadCached(); err == nil && isUsable(cached, proxyURL, clientID, topics, capabilities, exposeAmount) { + return &cached.Session, true, nil + } + + sess, err := CreateSession(proxyURL, clientID, accessToken, topics, capabilities, exposeAmount) + if err != nil { + return nil, false, err + } + + c := &CachedSession{ + ProxyURL: proxyURL, + ClientID: clientID, + Topics: topics, + Capabilities: capabilities, + ExposeAmount: exposeAmount, + Session: *sess, + } + if saveErr := saveCached(c); saveErr != nil { + fmt.Printf("Warning: could not cache session: %v\n", saveErr) + } + + return sess, false, nil +} + +// InvalidateSession removes the cached session file. +func InvalidateSession() { + p, err := cachePath() + if err != nil { + return + } + os.Remove(p) +} diff --git a/proto/p2p_stream.pb.go b/proto/p2p_stream.pb.go new file mode 100644 index 0000000..01d2213 --- /dev/null +++ b/proto/p2p_stream.pb.go @@ -0,0 +1,486 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc v5.29.3 +// source: p2p_stream.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ResponseType int32 + +const ( + ResponseType_Unknown ResponseType = 0 + ResponseType_Message ResponseType = 1 + ResponseType_MessageTraceMumP2P ResponseType = 2 + ResponseType_MessageTraceGossipSub ResponseType = 3 +) + +// Enum value maps for ResponseType. +var ( + ResponseType_name = map[int32]string{ + 0: "Unknown", + 1: "Message", + 2: "MessageTraceMumP2P", + 3: "MessageTraceGossipSub", + } + ResponseType_value = map[string]int32{ + "Unknown": 0, + "Message": 1, + "MessageTraceMumP2P": 2, + "MessageTraceGossipSub": 3, + } +) + +func (x ResponseType) Enum() *ResponseType { + p := new(ResponseType) + *p = x + return p +} + +func (x ResponseType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ResponseType) Descriptor() protoreflect.EnumDescriptor { + return file_p2p_stream_proto_enumTypes[0].Descriptor() +} + +func (ResponseType) Type() protoreflect.EnumType { + return &file_p2p_stream_proto_enumTypes[0] +} + +func (x ResponseType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ResponseType.Descriptor instead. +func (ResponseType) EnumDescriptor() ([]byte, []int) { + return file_p2p_stream_proto_rawDescGZIP(), []int{0} +} + +type Void struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Void) Reset() { + *x = Void{} + mi := &file_p2p_stream_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Void) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Void) ProtoMessage() {} + +func (x *Void) ProtoReflect() protoreflect.Message { + mi := &file_p2p_stream_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Void.ProtoReflect.Descriptor instead. +func (*Void) Descriptor() ([]byte, []int) { + return file_p2p_stream_proto_rawDescGZIP(), []int{0} +} + +type HealthResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status bool `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` + NodeMode string `protobuf:"bytes,2,opt,name=nodeMode,proto3" json:"nodeMode,omitempty"` + MemoryUsed float32 `protobuf:"fixed32,3,opt,name=memoryUsed,proto3" json:"memoryUsed,omitempty"` + CpuUsed float32 `protobuf:"fixed32,4,opt,name=cpuUsed,proto3" json:"cpuUsed,omitempty"` + DiskUsed float32 `protobuf:"fixed32,5,opt,name=diskUsed,proto3" json:"diskUsed,omitempty"` + P2PAddress string `protobuf:"bytes,6,opt,name=p2pAddress,proto3" json:"p2pAddress,omitempty"` + Country string `protobuf:"bytes,7,opt,name=country,proto3" json:"country,omitempty"` + CountryIso string `protobuf:"bytes,8,opt,name=country_iso,json=countryIso,proto3" json:"country_iso,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthResponse) Reset() { + *x = HealthResponse{} + mi := &file_p2p_stream_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthResponse) ProtoMessage() {} + +func (x *HealthResponse) ProtoReflect() protoreflect.Message { + mi := &file_p2p_stream_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthResponse.ProtoReflect.Descriptor instead. +func (*HealthResponse) Descriptor() ([]byte, []int) { + return file_p2p_stream_proto_rawDescGZIP(), []int{1} +} + +func (x *HealthResponse) GetStatus() bool { + if x != nil { + return x.Status + } + return false +} + +func (x *HealthResponse) GetNodeMode() string { + if x != nil { + return x.NodeMode + } + return "" +} + +func (x *HealthResponse) GetMemoryUsed() float32 { + if x != nil { + return x.MemoryUsed + } + return 0 +} + +func (x *HealthResponse) GetCpuUsed() float32 { + if x != nil { + return x.CpuUsed + } + return 0 +} + +func (x *HealthResponse) GetDiskUsed() float32 { + if x != nil { + return x.DiskUsed + } + return 0 +} + +func (x *HealthResponse) GetP2PAddress() string { + if x != nil { + return x.P2PAddress + } + return "" +} + +func (x *HealthResponse) GetCountry() string { + if x != nil { + return x.Country + } + return "" +} + +func (x *HealthResponse) GetCountryIso() string { + if x != nil { + return x.CountryIso + } + return "" +} + +type Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + Command int32 `protobuf:"varint,1,opt,name=command,proto3" json:"command,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Topic string `protobuf:"bytes,3,opt,name=topic,proto3" json:"topic,omitempty"` + JwtToken string `protobuf:"bytes,4,opt,name=jwt_token,json=jwtToken,proto3" json:"jwt_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Request) Reset() { + *x = Request{} + mi := &file_p2p_stream_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Request) ProtoMessage() {} + +func (x *Request) ProtoReflect() protoreflect.Message { + mi := &file_p2p_stream_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Request.ProtoReflect.Descriptor instead. +func (*Request) Descriptor() ([]byte, []int) { + return file_p2p_stream_proto_rawDescGZIP(), []int{2} +} + +func (x *Request) GetCommand() int32 { + if x != nil { + return x.Command + } + return 0 +} + +func (x *Request) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *Request) GetTopic() string { + if x != nil { + return x.Topic + } + return "" +} + +func (x *Request) GetJwtToken() string { + if x != nil { + return x.JwtToken + } + return "" +} + +type Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Command ResponseType `protobuf:"varint,1,opt,name=command,proto3,enum=proto.ResponseType" json:"command,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Metadata []byte `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Response) Reset() { + *x = Response{} + mi := &file_p2p_stream_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Response) ProtoMessage() {} + +func (x *Response) ProtoReflect() protoreflect.Message { + mi := &file_p2p_stream_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Response.ProtoReflect.Descriptor instead. +func (*Response) Descriptor() ([]byte, []int) { + return file_p2p_stream_proto_rawDescGZIP(), []int{3} +} + +func (x *Response) GetCommand() ResponseType { + if x != nil { + return x.Command + } + return ResponseType_Unknown +} + +func (x *Response) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *Response) GetMetadata() []byte { + if x != nil { + return x.Metadata + } + return nil +} + +type TopicList struct { + state protoimpl.MessageState `protogen:"open.v1"` + Topics []string `protobuf:"bytes,1,rep,name=topics,proto3" json:"topics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TopicList) Reset() { + *x = TopicList{} + mi := &file_p2p_stream_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TopicList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TopicList) ProtoMessage() {} + +func (x *TopicList) ProtoReflect() protoreflect.Message { + mi := &file_p2p_stream_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TopicList.ProtoReflect.Descriptor instead. +func (*TopicList) Descriptor() ([]byte, []int) { + return file_p2p_stream_proto_rawDescGZIP(), []int{4} +} + +func (x *TopicList) GetTopics() []string { + if x != nil { + return x.Topics + } + return nil +} + +var File_p2p_stream_proto protoreflect.FileDescriptor + +const file_p2p_stream_proto_rawDesc = "" + + "\n" + + "\x10p2p_stream.proto\x12\x05proto\"\x06\n" + + "\x04Void\"\xf5\x01\n" + + "\x0eHealthResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\bR\x06status\x12\x1a\n" + + "\bnodeMode\x18\x02 \x01(\tR\bnodeMode\x12\x1e\n" + + "\n" + + "memoryUsed\x18\x03 \x01(\x02R\n" + + "memoryUsed\x12\x18\n" + + "\acpuUsed\x18\x04 \x01(\x02R\acpuUsed\x12\x1a\n" + + "\bdiskUsed\x18\x05 \x01(\x02R\bdiskUsed\x12\x1e\n" + + "\n" + + "p2pAddress\x18\x06 \x01(\tR\n" + + "p2pAddress\x12\x18\n" + + "\acountry\x18\a \x01(\tR\acountry\x12\x1f\n" + + "\vcountry_iso\x18\b \x01(\tR\n" + + "countryIso\"j\n" + + "\aRequest\x12\x18\n" + + "\acommand\x18\x01 \x01(\x05R\acommand\x12\x12\n" + + "\x04data\x18\x02 \x01(\fR\x04data\x12\x14\n" + + "\x05topic\x18\x03 \x01(\tR\x05topic\x12\x1b\n" + + "\tjwt_token\x18\x04 \x01(\tR\bjwtToken\"i\n" + + "\bResponse\x12-\n" + + "\acommand\x18\x01 \x01(\x0e2\x13.proto.ResponseTypeR\acommand\x12\x12\n" + + "\x04data\x18\x02 \x01(\fR\x04data\x12\x1a\n" + + "\bmetadata\x18\x03 \x01(\fR\bmetadata\"#\n" + + "\tTopicList\x12\x16\n" + + "\x06topics\x18\x01 \x03(\tR\x06topics*[\n" + + "\fResponseType\x12\v\n" + + "\aUnknown\x10\x00\x12\v\n" + + "\aMessage\x10\x01\x12\x16\n" + + "\x12MessageTraceMumP2P\x10\x02\x12\x19\n" + + "\x15MessageTraceGossipSub\x10\x032\xa7\x01\n" + + "\rCommandStream\x127\n" + + "\x0eListenCommands\x12\x0e.proto.Request\x1a\x0f.proto.Response\"\x00(\x010\x01\x12.\n" + + "\x06Health\x12\v.proto.Void\x1a\x15.proto.HealthResponse\"\x00\x12-\n" + + "\n" + + "ListTopics\x12\v.proto.Void\x1a\x10.proto.TopicList\"\x00B.Z,github.com/getoptimum/mump2p-cli/proto;protob\x06proto3" + +var ( + file_p2p_stream_proto_rawDescOnce sync.Once + file_p2p_stream_proto_rawDescData []byte +) + +func file_p2p_stream_proto_rawDescGZIP() []byte { + file_p2p_stream_proto_rawDescOnce.Do(func() { + file_p2p_stream_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_p2p_stream_proto_rawDesc), len(file_p2p_stream_proto_rawDesc))) + }) + return file_p2p_stream_proto_rawDescData +} + +var file_p2p_stream_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_p2p_stream_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_p2p_stream_proto_goTypes = []any{ + (ResponseType)(0), // 0: proto.ResponseType + (*Void)(nil), // 1: proto.Void + (*HealthResponse)(nil), // 2: proto.HealthResponse + (*Request)(nil), // 3: proto.Request + (*Response)(nil), // 4: proto.Response + (*TopicList)(nil), // 5: proto.TopicList +} +var file_p2p_stream_proto_depIdxs = []int32{ + 0, // 0: proto.Response.command:type_name -> proto.ResponseType + 3, // 1: proto.CommandStream.ListenCommands:input_type -> proto.Request + 1, // 2: proto.CommandStream.Health:input_type -> proto.Void + 1, // 3: proto.CommandStream.ListTopics:input_type -> proto.Void + 4, // 4: proto.CommandStream.ListenCommands:output_type -> proto.Response + 2, // 5: proto.CommandStream.Health:output_type -> proto.HealthResponse + 5, // 6: proto.CommandStream.ListTopics:output_type -> proto.TopicList + 4, // [4:7] is the sub-list for method output_type + 1, // [1:4] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_p2p_stream_proto_init() } +func file_p2p_stream_proto_init() { + if File_p2p_stream_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_p2p_stream_proto_rawDesc), len(file_p2p_stream_proto_rawDesc)), + NumEnums: 1, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_p2p_stream_proto_goTypes, + DependencyIndexes: file_p2p_stream_proto_depIdxs, + EnumInfos: file_p2p_stream_proto_enumTypes, + MessageInfos: file_p2p_stream_proto_msgTypes, + }.Build() + File_p2p_stream_proto = out.File + file_p2p_stream_proto_goTypes = nil + file_p2p_stream_proto_depIdxs = nil +} diff --git a/proto/p2p_stream.proto b/proto/p2p_stream.proto new file mode 100644 index 0000000..563760c --- /dev/null +++ b/proto/p2p_stream.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package proto; + +option go_package = "github.com/getoptimum/mump2p-cli/proto;proto"; + +service CommandStream { + rpc ListenCommands (stream Request) returns (stream Response) {} + rpc Health (Void) returns (HealthResponse) {} + rpc ListTopics (Void) returns (TopicList) {} +} + +message Void {} + +message HealthResponse { + bool status = 1; + string nodeMode = 2; + float memoryUsed = 3; + float cpuUsed = 4; + float diskUsed = 5; + string p2pAddress = 6; + string country = 7; + string country_iso = 8; +} + +enum ResponseType { + Unknown = 0; + Message = 1; + MessageTraceMumP2P = 2; + MessageTraceGossipSub = 3; +} + +message Request { + int32 command = 1; + bytes data = 2; + string topic = 3; + string jwt_token = 4; +} + +message Response { + ResponseType command = 1; + bytes data = 2; + bytes metadata = 3; +} + +message TopicList { + repeated string topics = 1; +} diff --git a/proto/p2p_stream_grpc.pb.go b/proto/p2p_stream_grpc.pb.go new file mode 100644 index 0000000..2f5fc01 --- /dev/null +++ b/proto/p2p_stream_grpc.pb.go @@ -0,0 +1,206 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// CommandStreamClient is the client API for CommandStream service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type CommandStreamClient interface { + ListenCommands(ctx context.Context, opts ...grpc.CallOption) (CommandStream_ListenCommandsClient, error) + Health(ctx context.Context, in *Void, opts ...grpc.CallOption) (*HealthResponse, error) + ListTopics(ctx context.Context, in *Void, opts ...grpc.CallOption) (*TopicList, error) +} + +type commandStreamClient struct { + cc grpc.ClientConnInterface +} + +func NewCommandStreamClient(cc grpc.ClientConnInterface) CommandStreamClient { + return &commandStreamClient{cc} +} + +func (c *commandStreamClient) ListenCommands(ctx context.Context, opts ...grpc.CallOption) (CommandStream_ListenCommandsClient, error) { + stream, err := c.cc.NewStream(ctx, &CommandStream_ServiceDesc.Streams[0], "/proto.CommandStream/ListenCommands", opts...) + if err != nil { + return nil, err + } + x := &commandStreamListenCommandsClient{stream} + return x, nil +} + +type CommandStream_ListenCommandsClient interface { + Send(*Request) error + Recv() (*Response, error) + grpc.ClientStream +} + +type commandStreamListenCommandsClient struct { + grpc.ClientStream +} + +func (x *commandStreamListenCommandsClient) Send(m *Request) error { + return x.ClientStream.SendMsg(m) +} + +func (x *commandStreamListenCommandsClient) Recv() (*Response, error) { + m := new(Response) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *commandStreamClient) Health(ctx context.Context, in *Void, opts ...grpc.CallOption) (*HealthResponse, error) { + out := new(HealthResponse) + err := c.cc.Invoke(ctx, "/proto.CommandStream/Health", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *commandStreamClient) ListTopics(ctx context.Context, in *Void, opts ...grpc.CallOption) (*TopicList, error) { + out := new(TopicList) + err := c.cc.Invoke(ctx, "/proto.CommandStream/ListTopics", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CommandStreamServer is the server API for CommandStream service. +// All implementations must embed UnimplementedCommandStreamServer +// for forward compatibility +type CommandStreamServer interface { + ListenCommands(CommandStream_ListenCommandsServer) error + Health(context.Context, *Void) (*HealthResponse, error) + ListTopics(context.Context, *Void) (*TopicList, error) + mustEmbedUnimplementedCommandStreamServer() +} + +// UnimplementedCommandStreamServer must be embedded to have forward compatible implementations. +type UnimplementedCommandStreamServer struct { +} + +func (UnimplementedCommandStreamServer) ListenCommands(CommandStream_ListenCommandsServer) error { + return status.Errorf(codes.Unimplemented, "method ListenCommands not implemented") +} +func (UnimplementedCommandStreamServer) Health(context.Context, *Void) (*HealthResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Health not implemented") +} +func (UnimplementedCommandStreamServer) ListTopics(context.Context, *Void) (*TopicList, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListTopics not implemented") +} +func (UnimplementedCommandStreamServer) mustEmbedUnimplementedCommandStreamServer() {} + +// UnsafeCommandStreamServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CommandStreamServer will +// result in compilation errors. +type UnsafeCommandStreamServer interface { + mustEmbedUnimplementedCommandStreamServer() +} + +func RegisterCommandStreamServer(s grpc.ServiceRegistrar, srv CommandStreamServer) { + s.RegisterService(&CommandStream_ServiceDesc, srv) +} + +func _CommandStream_ListenCommands_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(CommandStreamServer).ListenCommands(&commandStreamListenCommandsServer{stream}) +} + +type CommandStream_ListenCommandsServer interface { + Send(*Response) error + Recv() (*Request, error) + grpc.ServerStream +} + +type commandStreamListenCommandsServer struct { + grpc.ServerStream +} + +func (x *commandStreamListenCommandsServer) Send(m *Response) error { + return x.ServerStream.SendMsg(m) +} + +func (x *commandStreamListenCommandsServer) Recv() (*Request, error) { + m := new(Request) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func _CommandStream_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Void) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CommandStreamServer).Health(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.CommandStream/Health", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CommandStreamServer).Health(ctx, req.(*Void)) + } + return interceptor(ctx, in, info, handler) +} + +func _CommandStream_ListTopics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Void) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CommandStreamServer).ListTopics(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.CommandStream/ListTopics", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CommandStreamServer).ListTopics(ctx, req.(*Void)) + } + return interceptor(ctx, in, info, handler) +} + +// CommandStream_ServiceDesc is the grpc.ServiceDesc for CommandStream service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var CommandStream_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.CommandStream", + HandlerType: (*CommandStreamServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Health", + Handler: _CommandStream_Health_Handler, + }, + { + MethodName: "ListTopics", + Handler: _CommandStream_ListTopics_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "ListenCommands", + Handler: _CommandStream_ListenCommands_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "p2p_stream.proto", +} diff --git a/proto/proxy_stream.pb.go b/proto/proxy_stream.pb.go deleted file mode 100644 index 3fbceb5..0000000 --- a/proto/proxy_stream.pb.go +++ /dev/null @@ -1,565 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.6 -// protoc v5.29.3 -// source: proto/proxy_stream.proto - -package proto - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type ExposeNodesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - ExposeAmount uint32 `protobuf:"varint,2,opt,name=expose_amount,json=exposeAmount,proto3" json:"expose_amount,omitempty"` // e.g., "10" for 10 nodes - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ExposeNodesRequest) Reset() { - *x = ExposeNodesRequest{} - mi := &file_proto_proxy_stream_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ExposeNodesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ExposeNodesRequest) ProtoMessage() {} - -func (x *ExposeNodesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_proxy_stream_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ExposeNodesRequest.ProtoReflect.Descriptor instead. -func (*ExposeNodesRequest) Descriptor() ([]byte, []int) { - return file_proto_proxy_stream_proto_rawDescGZIP(), []int{0} -} - -func (x *ExposeNodesRequest) GetClientId() string { - if x != nil { - return x.ClientId - } - return "" -} - -func (x *ExposeNodesRequest) GetExposeAmount() uint32 { - if x != nil { - return x.ExposeAmount - } - return 0 -} - -type ExposeNodesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Nodes []string `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` // List of exposed nodes sidecars - NodesOptP2P []string `protobuf:"bytes,2,rep,name=nodesOptP2P,proto3" json:"nodesOptP2P,omitempty"` // List of exposed nodes libP2P - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ExposeNodesResponse) Reset() { - *x = ExposeNodesResponse{} - mi := &file_proto_proxy_stream_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ExposeNodesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ExposeNodesResponse) ProtoMessage() {} - -func (x *ExposeNodesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_proxy_stream_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ExposeNodesResponse.ProtoReflect.Descriptor instead. -func (*ExposeNodesResponse) Descriptor() ([]byte, []int) { - return file_proto_proxy_stream_proto_rawDescGZIP(), []int{1} -} - -func (x *ExposeNodesResponse) GetNodes() []string { - if x != nil { - return x.Nodes - } - return nil -} - -func (x *ExposeNodesResponse) GetNodesOptP2P() []string { - if x != nil { - return x.NodesOptP2P - } - return nil -} - -// ProxyMessage represents a message sent between the Optimum Proxy and clients. -type ProxyMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - Message []byte `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - Topic string `protobuf:"bytes,3,opt,name=topic,proto3" json:"topic,omitempty"` - MessageId string `protobuf:"bytes,4,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` - Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ProxyMessage) Reset() { - *x = ProxyMessage{} - mi := &file_proto_proxy_stream_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ProxyMessage) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ProxyMessage) ProtoMessage() {} - -func (x *ProxyMessage) ProtoReflect() protoreflect.Message { - mi := &file_proto_proxy_stream_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ProxyMessage.ProtoReflect.Descriptor instead. -func (*ProxyMessage) Descriptor() ([]byte, []int) { - return file_proto_proxy_stream_proto_rawDescGZIP(), []int{2} -} - -func (x *ProxyMessage) GetClientId() string { - if x != nil { - return x.ClientId - } - return "" -} - -func (x *ProxyMessage) GetMessage() []byte { - if x != nil { - return x.Message - } - return nil -} - -func (x *ProxyMessage) GetTopic() string { - if x != nil { - return x.Topic - } - return "" -} - -func (x *ProxyMessage) GetMessageId() string { - if x != nil { - return x.MessageId - } - return "" -} - -func (x *ProxyMessage) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -// SubscribeRequest represents a request to subscribe to a topic. -type SubscribeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` // ID of the client subscribing to the topic - Topic string `protobuf:"bytes,2,opt,name=topic,proto3" json:"topic,omitempty"` // Topic to which the client wants to subscribe - Threshold float32 `protobuf:"fixed32,3,opt,name=threshold,proto3" json:"threshold,omitempty"` // Optional threshold for the subscription, e.g., "0.5" for 50% of messages - Topics []string `protobuf:"bytes,4,rep,name=topics,proto3" json:"topics,omitempty"` // List of topics to which the client wants to subscribe - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SubscribeRequest) Reset() { - *x = SubscribeRequest{} - mi := &file_proto_proxy_stream_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SubscribeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SubscribeRequest) ProtoMessage() {} - -func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_proxy_stream_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SubscribeRequest.ProtoReflect.Descriptor instead. -func (*SubscribeRequest) Descriptor() ([]byte, []int) { - return file_proto_proxy_stream_proto_rawDescGZIP(), []int{3} -} - -func (x *SubscribeRequest) GetClientId() string { - if x != nil { - return x.ClientId - } - return "" -} - -func (x *SubscribeRequest) GetTopic() string { - if x != nil { - return x.Topic - } - return "" -} - -func (x *SubscribeRequest) GetThreshold() float32 { - if x != nil { - return x.Threshold - } - return 0 -} - -func (x *SubscribeRequest) GetTopics() []string { - if x != nil { - return x.Topics - } - return nil -} - -type SubscribeResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` // e.g., "subscribed", "unsubscribed", "error" - Topic string `protobuf:"bytes,2,opt,name=topic,proto3" json:"topic,omitempty"` // The topic to which the client is subscribed - ClientId string `protobuf:"bytes,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` // The ID of the client that subscribed - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SubscribeResponse) Reset() { - *x = SubscribeResponse{} - mi := &file_proto_proxy_stream_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SubscribeResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SubscribeResponse) ProtoMessage() {} - -func (x *SubscribeResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_proxy_stream_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SubscribeResponse.ProtoReflect.Descriptor instead. -func (*SubscribeResponse) Descriptor() ([]byte, []int) { - return file_proto_proxy_stream_proto_rawDescGZIP(), []int{4} -} - -func (x *SubscribeResponse) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *SubscribeResponse) GetTopic() string { - if x != nil { - return x.Topic - } - return "" -} - -func (x *SubscribeResponse) GetClientId() string { - if x != nil { - return x.ClientId - } - return "" -} - -type PublishRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` // ID of the client publishing the message - Topic string `protobuf:"bytes,2,opt,name=topic,proto3" json:"topic,omitempty"` // Topic to which the message is published - Message []byte `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` // The actual message content - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PublishRequest) Reset() { - *x = PublishRequest{} - mi := &file_proto_proxy_stream_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PublishRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PublishRequest) ProtoMessage() {} - -func (x *PublishRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_proxy_stream_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PublishRequest.ProtoReflect.Descriptor instead. -func (*PublishRequest) Descriptor() ([]byte, []int) { - return file_proto_proxy_stream_proto_rawDescGZIP(), []int{5} -} - -func (x *PublishRequest) GetClientId() string { - if x != nil { - return x.ClientId - } - return "" -} - -func (x *PublishRequest) GetTopic() string { - if x != nil { - return x.Topic - } - return "" -} - -func (x *PublishRequest) GetMessage() []byte { - if x != nil { - return x.Message - } - return nil -} - -type PublishResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` // e.g., "published", "error" - Topic string `protobuf:"bytes,2,opt,name=topic,proto3" json:"topic,omitempty"` // The topic to which the message was published - ClientId string `protobuf:"bytes,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` // The ID of the client that published the message - MessageId string `protobuf:"bytes,4,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` // Unique identifier for the published message - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PublishResponse) Reset() { - *x = PublishResponse{} - mi := &file_proto_proxy_stream_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PublishResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PublishResponse) ProtoMessage() {} - -func (x *PublishResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_proxy_stream_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PublishResponse.ProtoReflect.Descriptor instead. -func (*PublishResponse) Descriptor() ([]byte, []int) { - return file_proto_proxy_stream_proto_rawDescGZIP(), []int{6} -} - -func (x *PublishResponse) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *PublishResponse) GetTopic() string { - if x != nil { - return x.Topic - } - return "" -} - -func (x *PublishResponse) GetClientId() string { - if x != nil { - return x.ClientId - } - return "" -} - -func (x *PublishResponse) GetMessageId() string { - if x != nil { - return x.MessageId - } - return "" -} - -var File_proto_proxy_stream_proto protoreflect.FileDescriptor - -const file_proto_proxy_stream_proto_rawDesc = "" + - "\n" + - "\x18proto/proxy_stream.proto\x12\x05proto\"V\n" + - "\x12ExposeNodesRequest\x12\x1b\n" + - "\tclient_id\x18\x01 \x01(\tR\bclientId\x12#\n" + - "\rexpose_amount\x18\x02 \x01(\rR\fexposeAmount\"M\n" + - "\x13ExposeNodesResponse\x12\x14\n" + - "\x05nodes\x18\x01 \x03(\tR\x05nodes\x12 \n" + - "\vnodesOptP2P\x18\x02 \x03(\tR\vnodesOptP2P\"\x8e\x01\n" + - "\fProxyMessage\x12\x1b\n" + - "\tclient_id\x18\x01 \x01(\tR\bclientId\x12\x18\n" + - "\amessage\x18\x02 \x01(\fR\amessage\x12\x14\n" + - "\x05topic\x18\x03 \x01(\tR\x05topic\x12\x1d\n" + - "\n" + - "message_id\x18\x04 \x01(\tR\tmessageId\x12\x12\n" + - "\x04type\x18\x05 \x01(\tR\x04type\"{\n" + - "\x10SubscribeRequest\x12\x1b\n" + - "\tclient_id\x18\x01 \x01(\tR\bclientId\x12\x14\n" + - "\x05topic\x18\x02 \x01(\tR\x05topic\x12\x1c\n" + - "\tthreshold\x18\x03 \x01(\x02R\tthreshold\x12\x16\n" + - "\x06topics\x18\x04 \x03(\tR\x06topics\"^\n" + - "\x11SubscribeResponse\x12\x16\n" + - "\x06status\x18\x01 \x01(\tR\x06status\x12\x14\n" + - "\x05topic\x18\x02 \x01(\tR\x05topic\x12\x1b\n" + - "\tclient_id\x18\x03 \x01(\tR\bclientId\"]\n" + - "\x0ePublishRequest\x12\x1b\n" + - "\tclient_id\x18\x01 \x01(\tR\bclientId\x12\x14\n" + - "\x05topic\x18\x02 \x01(\tR\x05topic\x12\x18\n" + - "\amessage\x18\x03 \x01(\fR\amessage\"{\n" + - "\x0fPublishResponse\x12\x16\n" + - "\x06status\x18\x01 \x01(\tR\x06status\x12\x14\n" + - "\x05topic\x18\x02 \x01(\tR\x05topic\x12\x1b\n" + - "\tclient_id\x18\x03 \x01(\tR\bclientId\x12\x1d\n" + - "\n" + - "message_id\x18\x04 \x01(\tR\tmessageId2\x8b\x02\n" + - "\vProxyStream\x12<\n" + - "\fClientStream\x12\x13.proto.ProxyMessage\x1a\x13.proto.ProxyMessage(\x010\x01\x128\n" + - "\aPublish\x12\x15.proto.PublishRequest\x1a\x16.proto.PublishResponse\x12>\n" + - "\tSubscribe\x12\x17.proto.SubscribeRequest\x1a\x18.proto.SubscribeResponse\x12D\n" + - "\vExposeNodes\x12\x19.proto.ExposeNodesRequest\x1a\x1a.proto.ExposeNodesResponseB.Z,github.com/getoptimum/mump2p-cli/proto;protob\x06proto3" - -var ( - file_proto_proxy_stream_proto_rawDescOnce sync.Once - file_proto_proxy_stream_proto_rawDescData []byte -) - -func file_proto_proxy_stream_proto_rawDescGZIP() []byte { - file_proto_proxy_stream_proto_rawDescOnce.Do(func() { - file_proto_proxy_stream_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_proxy_stream_proto_rawDesc), len(file_proto_proxy_stream_proto_rawDesc))) - }) - return file_proto_proxy_stream_proto_rawDescData -} - -var file_proto_proxy_stream_proto_msgTypes = make([]protoimpl.MessageInfo, 7) -var file_proto_proxy_stream_proto_goTypes = []any{ - (*ExposeNodesRequest)(nil), // 0: proto.ExposeNodesRequest - (*ExposeNodesResponse)(nil), // 1: proto.ExposeNodesResponse - (*ProxyMessage)(nil), // 2: proto.ProxyMessage - (*SubscribeRequest)(nil), // 3: proto.SubscribeRequest - (*SubscribeResponse)(nil), // 4: proto.SubscribeResponse - (*PublishRequest)(nil), // 5: proto.PublishRequest - (*PublishResponse)(nil), // 6: proto.PublishResponse -} -var file_proto_proxy_stream_proto_depIdxs = []int32{ - 2, // 0: proto.ProxyStream.ClientStream:input_type -> proto.ProxyMessage - 5, // 1: proto.ProxyStream.Publish:input_type -> proto.PublishRequest - 3, // 2: proto.ProxyStream.Subscribe:input_type -> proto.SubscribeRequest - 0, // 3: proto.ProxyStream.ExposeNodes:input_type -> proto.ExposeNodesRequest - 2, // 4: proto.ProxyStream.ClientStream:output_type -> proto.ProxyMessage - 6, // 5: proto.ProxyStream.Publish:output_type -> proto.PublishResponse - 4, // 6: proto.ProxyStream.Subscribe:output_type -> proto.SubscribeResponse - 1, // 7: proto.ProxyStream.ExposeNodes:output_type -> proto.ExposeNodesResponse - 4, // [4:8] is the sub-list for method output_type - 0, // [0:4] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_proto_proxy_stream_proto_init() } -func file_proto_proxy_stream_proto_init() { - if File_proto_proxy_stream_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_proxy_stream_proto_rawDesc), len(file_proto_proxy_stream_proto_rawDesc)), - NumEnums: 0, - NumMessages: 7, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_proto_proxy_stream_proto_goTypes, - DependencyIndexes: file_proto_proxy_stream_proto_depIdxs, - MessageInfos: file_proto_proxy_stream_proto_msgTypes, - }.Build() - File_proto_proxy_stream_proto = out.File - file_proto_proxy_stream_proto_goTypes = nil - file_proto_proxy_stream_proto_depIdxs = nil -} diff --git a/proto/proxy_stream_grpc.pb.go b/proto/proxy_stream_grpc.pb.go deleted file mode 100644 index 2772e98..0000000 --- a/proto/proxy_stream_grpc.pb.go +++ /dev/null @@ -1,242 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 -// source: proto/proxy_stream.proto - -package proto - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - ProxyStream_ClientStream_FullMethodName = "/proto.ProxyStream/ClientStream" - ProxyStream_Publish_FullMethodName = "/proto.ProxyStream/Publish" - ProxyStream_Subscribe_FullMethodName = "/proto.ProxyStream/Subscribe" - ProxyStream_ExposeNodes_FullMethodName = "/proto.ProxyStream/ExposeNodes" -) - -// ProxyStreamClient is the client API for ProxyStream service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -// -// ProxyStream establishes a stream connection to send and receive messages -// between the Optimum Proxy and the clients. -type ProxyStreamClient interface { - ClientStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ProxyMessage, ProxyMessage], error) - // Publish allows clients to publish messages to a specific topic. - Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error) - // Subscribe allows clients to subscribe to a specific topic without establish stream. - Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (*SubscribeResponse, error) - // ExposeNodes allows clients to request a specific number of optP2P nodes to be exposed. - ExposeNodes(ctx context.Context, in *ExposeNodesRequest, opts ...grpc.CallOption) (*ExposeNodesResponse, error) -} - -type proxyStreamClient struct { - cc grpc.ClientConnInterface -} - -func NewProxyStreamClient(cc grpc.ClientConnInterface) ProxyStreamClient { - return &proxyStreamClient{cc} -} - -func (c *proxyStreamClient) ClientStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ProxyMessage, ProxyMessage], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &ProxyStream_ServiceDesc.Streams[0], ProxyStream_ClientStream_FullMethodName, cOpts...) - if err != nil { - return nil, err - } - x := &grpc.GenericClientStream[ProxyMessage, ProxyMessage]{ClientStream: stream} - return x, nil -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type ProxyStream_ClientStreamClient = grpc.BidiStreamingClient[ProxyMessage, ProxyMessage] - -func (c *proxyStreamClient) Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(PublishResponse) - err := c.cc.Invoke(ctx, ProxyStream_Publish_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *proxyStreamClient) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (*SubscribeResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(SubscribeResponse) - err := c.cc.Invoke(ctx, ProxyStream_Subscribe_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *proxyStreamClient) ExposeNodes(ctx context.Context, in *ExposeNodesRequest, opts ...grpc.CallOption) (*ExposeNodesResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ExposeNodesResponse) - err := c.cc.Invoke(ctx, ProxyStream_ExposeNodes_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// ProxyStreamServer is the server API for ProxyStream service. -// All implementations must embed UnimplementedProxyStreamServer -// for forward compatibility. -// -// ProxyStream establishes a stream connection to send and receive messages -// between the Optimum Proxy and the clients. -type ProxyStreamServer interface { - ClientStream(grpc.BidiStreamingServer[ProxyMessage, ProxyMessage]) error - // Publish allows clients to publish messages to a specific topic. - Publish(context.Context, *PublishRequest) (*PublishResponse, error) - // Subscribe allows clients to subscribe to a specific topic without establish stream. - Subscribe(context.Context, *SubscribeRequest) (*SubscribeResponse, error) - // ExposeNodes allows clients to request a specific number of optP2P nodes to be exposed. - ExposeNodes(context.Context, *ExposeNodesRequest) (*ExposeNodesResponse, error) - mustEmbedUnimplementedProxyStreamServer() -} - -// UnimplementedProxyStreamServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedProxyStreamServer struct{} - -func (UnimplementedProxyStreamServer) ClientStream(grpc.BidiStreamingServer[ProxyMessage, ProxyMessage]) error { - return status.Errorf(codes.Unimplemented, "method ClientStream not implemented") -} -func (UnimplementedProxyStreamServer) Publish(context.Context, *PublishRequest) (*PublishResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Publish not implemented") -} -func (UnimplementedProxyStreamServer) Subscribe(context.Context, *SubscribeRequest) (*SubscribeResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Subscribe not implemented") -} -func (UnimplementedProxyStreamServer) ExposeNodes(context.Context, *ExposeNodesRequest) (*ExposeNodesResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ExposeNodes not implemented") -} -func (UnimplementedProxyStreamServer) mustEmbedUnimplementedProxyStreamServer() {} -func (UnimplementedProxyStreamServer) testEmbeddedByValue() {} - -// UnsafeProxyStreamServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to ProxyStreamServer will -// result in compilation errors. -type UnsafeProxyStreamServer interface { - mustEmbedUnimplementedProxyStreamServer() -} - -func RegisterProxyStreamServer(s grpc.ServiceRegistrar, srv ProxyStreamServer) { - // If the following call pancis, it indicates UnimplementedProxyStreamServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&ProxyStream_ServiceDesc, srv) -} - -func _ProxyStream_ClientStream_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(ProxyStreamServer).ClientStream(&grpc.GenericServerStream[ProxyMessage, ProxyMessage]{ServerStream: stream}) -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type ProxyStream_ClientStreamServer = grpc.BidiStreamingServer[ProxyMessage, ProxyMessage] - -func _ProxyStream_Publish_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(PublishRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ProxyStreamServer).Publish(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: ProxyStream_Publish_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ProxyStreamServer).Publish(ctx, req.(*PublishRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ProxyStream_Subscribe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SubscribeRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ProxyStreamServer).Subscribe(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: ProxyStream_Subscribe_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ProxyStreamServer).Subscribe(ctx, req.(*SubscribeRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ProxyStream_ExposeNodes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ExposeNodesRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ProxyStreamServer).ExposeNodes(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: ProxyStream_ExposeNodes_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ProxyStreamServer).ExposeNodes(ctx, req.(*ExposeNodesRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// ProxyStream_ServiceDesc is the grpc.ServiceDesc for ProxyStream service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var ProxyStream_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "proto.ProxyStream", - HandlerType: (*ProxyStreamServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Publish", - Handler: _ProxyStream_Publish_Handler, - }, - { - MethodName: "Subscribe", - Handler: _ProxyStream_Subscribe_Handler, - }, - { - MethodName: "ExposeNodes", - Handler: _ProxyStream_ExposeNodes_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "ClientStream", - Handler: _ProxyStream_ClientStream_Handler, - ServerStreams: true, - ClientStreams: true, - }, - }, - Metadata: "proto/proxy_stream.proto", -} diff --git a/proto/session.pb.go b/proto/session.pb.go new file mode 100644 index 0000000..a85e8f2 --- /dev/null +++ b/proto/session.pb.go @@ -0,0 +1,350 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc v5.29.3 +// source: session.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + Topics []string `protobuf:"bytes,2,rep,name=topics,proto3" json:"topics,omitempty"` + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` + RegionHint string `protobuf:"bytes,4,opt,name=region_hint,json=regionHint,proto3" json:"region_hint,omitempty"` + ExposeAmount uint32 `protobuf:"varint,5,opt,name=expose_amount,json=exposeAmount,proto3" json:"expose_amount,omitempty"` + Capabilities []string `protobuf:"bytes,6,rep,name=capabilities,proto3" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionRequest) Reset() { + *x = SessionRequest{} + mi := &file_session_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionRequest) ProtoMessage() {} + +func (x *SessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_session_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionRequest.ProtoReflect.Descriptor instead. +func (*SessionRequest) Descriptor() ([]byte, []int) { + return file_session_proto_rawDescGZIP(), []int{0} +} + +func (x *SessionRequest) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *SessionRequest) GetTopics() []string { + if x != nil { + return x.Topics + } + return nil +} + +func (x *SessionRequest) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *SessionRequest) GetRegionHint() string { + if x != nil { + return x.RegionHint + } + return "" +} + +func (x *SessionRequest) GetExposeAmount() uint32 { + if x != nil { + return x.ExposeAmount + } + return 0 +} + +func (x *SessionRequest) GetCapabilities() []string { + if x != nil { + return x.Capabilities + } + return nil +} + +type SessionNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + Transport string `protobuf:"bytes,3,opt,name=transport,proto3" json:"transport,omitempty"` + Region string `protobuf:"bytes,4,opt,name=region,proto3" json:"region,omitempty"` + Ticket string `protobuf:"bytes,5,opt,name=ticket,proto3" json:"ticket,omitempty"` + Score float32 `protobuf:"fixed32,6,opt,name=score,proto3" json:"score,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionNode) Reset() { + *x = SessionNode{} + mi := &file_session_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionNode) ProtoMessage() {} + +func (x *SessionNode) ProtoReflect() protoreflect.Message { + mi := &file_session_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionNode.ProtoReflect.Descriptor instead. +func (*SessionNode) Descriptor() ([]byte, []int) { + return file_session_proto_rawDescGZIP(), []int{1} +} + +func (x *SessionNode) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *SessionNode) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *SessionNode) GetTransport() string { + if x != nil { + return x.Transport + } + return "" +} + +func (x *SessionNode) GetRegion() string { + if x != nil { + return x.Region + } + return "" +} + +func (x *SessionNode) GetTicket() string { + if x != nil { + return x.Ticket + } + return "" +} + +func (x *SessionNode) GetScore() float32 { + if x != nil { + return x.Score + } + return 0 +} + +type SessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Nodes []*SessionNode `protobuf:"bytes,2,rep,name=nodes,proto3" json:"nodes,omitempty"` + ExpiresAt string `protobuf:"bytes,3,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + RefreshAfter string `protobuf:"bytes,4,opt,name=refresh_after,json=refreshAfter,proto3" json:"refresh_after,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionResponse) Reset() { + *x = SessionResponse{} + mi := &file_session_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionResponse) ProtoMessage() {} + +func (x *SessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_session_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionResponse.ProtoReflect.Descriptor instead. +func (*SessionResponse) Descriptor() ([]byte, []int) { + return file_session_proto_rawDescGZIP(), []int{2} +} + +func (x *SessionResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *SessionResponse) GetNodes() []*SessionNode { + if x != nil { + return x.Nodes + } + return nil +} + +func (x *SessionResponse) GetExpiresAt() string { + if x != nil { + return x.ExpiresAt + } + return "" +} + +func (x *SessionResponse) GetRefreshAfter() string { + if x != nil { + return x.RefreshAfter + } + return "" +} + +func (x *SessionResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +var File_session_proto protoreflect.FileDescriptor + +const file_session_proto_rawDesc = "" + + "\n" + + "\rsession.proto\x12\x05proto\"\xcb\x01\n" + + "\x0eSessionRequest\x12\x1b\n" + + "\tclient_id\x18\x01 \x01(\tR\bclientId\x12\x16\n" + + "\x06topics\x18\x02 \x03(\tR\x06topics\x12\x1a\n" + + "\bprotocol\x18\x03 \x01(\tR\bprotocol\x12\x1f\n" + + "\vregion_hint\x18\x04 \x01(\tR\n" + + "regionHint\x12#\n" + + "\rexpose_amount\x18\x05 \x01(\rR\fexposeAmount\x12\"\n" + + "\fcapabilities\x18\x06 \x03(\tR\fcapabilities\"\x9b\x01\n" + + "\vSessionNode\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" + + "\aaddress\x18\x02 \x01(\tR\aaddress\x12\x1c\n" + + "\ttransport\x18\x03 \x01(\tR\ttransport\x12\x16\n" + + "\x06region\x18\x04 \x01(\tR\x06region\x12\x16\n" + + "\x06ticket\x18\x05 \x01(\tR\x06ticket\x12\x14\n" + + "\x05score\x18\x06 \x01(\x02R\x05score\"\xb4\x01\n" + + "\x0fSessionResponse\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12(\n" + + "\x05nodes\x18\x02 \x03(\v2\x12.proto.SessionNodeR\x05nodes\x12\x1d\n" + + "\n" + + "expires_at\x18\x03 \x01(\tR\texpiresAt\x12#\n" + + "\rrefresh_after\x18\x04 \x01(\tR\frefreshAfter\x12\x14\n" + + "\x05error\x18\x05 \x01(\tR\x05error2P\n" + + "\x0eSessionService\x12>\n" + + "\rCreateSession\x12\x15.proto.SessionRequest\x1a\x16.proto.SessionResponseB.Z,github.com/getoptimum/mump2p-cli/proto;protob\x06proto3" + +var ( + file_session_proto_rawDescOnce sync.Once + file_session_proto_rawDescData []byte +) + +func file_session_proto_rawDescGZIP() []byte { + file_session_proto_rawDescOnce.Do(func() { + file_session_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_session_proto_rawDesc), len(file_session_proto_rawDesc))) + }) + return file_session_proto_rawDescData +} + +var file_session_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_session_proto_goTypes = []any{ + (*SessionRequest)(nil), // 0: proto.SessionRequest + (*SessionNode)(nil), // 1: proto.SessionNode + (*SessionResponse)(nil), // 2: proto.SessionResponse +} +var file_session_proto_depIdxs = []int32{ + 1, // 0: proto.SessionResponse.nodes:type_name -> proto.SessionNode + 0, // 1: proto.SessionService.CreateSession:input_type -> proto.SessionRequest + 2, // 2: proto.SessionService.CreateSession:output_type -> proto.SessionResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_session_proto_init() } +func file_session_proto_init() { + if File_session_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_session_proto_rawDesc), len(file_session_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_session_proto_goTypes, + DependencyIndexes: file_session_proto_depIdxs, + MessageInfos: file_session_proto_msgTypes, + }.Build() + File_session_proto = out.File + file_session_proto_goTypes = nil + file_session_proto_depIdxs = nil +} diff --git a/proto/session.proto b/proto/session.proto new file mode 100644 index 0000000..e713db1 --- /dev/null +++ b/proto/session.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package proto; + +option go_package = "github.com/getoptimum/mump2p-cli/proto;proto"; + +service SessionService { + rpc CreateSession(SessionRequest) returns (SessionResponse); +} + +message SessionRequest { + string client_id = 1; + repeated string topics = 2; + string protocol = 3; + string region_hint = 4; + uint32 expose_amount = 5; + repeated string capabilities = 6; +} + +message SessionNode { + string id = 1; + string address = 2; + string transport = 3; + string region = 4; + string ticket = 5; + float score = 6; +} + +message SessionResponse { + string session_id = 1; + repeated SessionNode nodes = 2; + string expires_at = 3; + string refresh_after = 4; + string error = 5; +} diff --git a/proto/session_grpc.pb.go b/proto/session_grpc.pb.go new file mode 100644 index 0000000..0eeb9b2 --- /dev/null +++ b/proto/session_grpc.pb.go @@ -0,0 +1,101 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// SessionServiceClient is the client API for SessionService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SessionServiceClient interface { + CreateSession(ctx context.Context, in *SessionRequest, opts ...grpc.CallOption) (*SessionResponse, error) +} + +type sessionServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewSessionServiceClient(cc grpc.ClientConnInterface) SessionServiceClient { + return &sessionServiceClient{cc} +} + +func (c *sessionServiceClient) CreateSession(ctx context.Context, in *SessionRequest, opts ...grpc.CallOption) (*SessionResponse, error) { + out := new(SessionResponse) + err := c.cc.Invoke(ctx, "/proto.SessionService/CreateSession", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SessionServiceServer is the server API for SessionService service. +// All implementations must embed UnimplementedSessionServiceServer +// for forward compatibility +type SessionServiceServer interface { + CreateSession(context.Context, *SessionRequest) (*SessionResponse, error) + mustEmbedUnimplementedSessionServiceServer() +} + +// UnimplementedSessionServiceServer must be embedded to have forward compatible implementations. +type UnimplementedSessionServiceServer struct { +} + +func (UnimplementedSessionServiceServer) CreateSession(context.Context, *SessionRequest) (*SessionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateSession not implemented") +} +func (UnimplementedSessionServiceServer) mustEmbedUnimplementedSessionServiceServer() {} + +// UnsafeSessionServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SessionServiceServer will +// result in compilation errors. +type UnsafeSessionServiceServer interface { + mustEmbedUnimplementedSessionServiceServer() +} + +func RegisterSessionServiceServer(s grpc.ServiceRegistrar, srv SessionServiceServer) { + s.RegisterService(&SessionService_ServiceDesc, srv) +} + +func _SessionService_CreateSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SessionServiceServer).CreateSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.SessionService/CreateSession", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SessionServiceServer).CreateSession(ctx, req.(*SessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SessionService_ServiceDesc is the grpc.ServiceDesc for SessionService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SessionService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.SessionService", + HandlerType: (*SessionServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateSession", + Handler: _SessionService_CreateSession_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "session.proto", +}