From 903bef14a37b30830a26d878e5c53e14c86e3de7 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Thu, 22 Jan 2026 17:13:08 +0200 Subject: [PATCH] fixed some more tests --- e2e/env.go | 67 +++++++++++++- e2e/namespace_isolation_test.go | 92 +++++++++++++++---- pkg/auth/simple_auth.go | 22 +++-- pkg/gateway/dependencies.go | 1 + pkg/gateway/handlers/cache/delete_handler.go | 10 +- pkg/gateway/handlers/cache/get_handler.go | 20 +++- pkg/gateway/handlers/cache/list_handler.go | 10 +- pkg/gateway/handlers/cache/set_handler.go | 20 +++- .../handlers/deployments/static_handler.go | 13 ++- pkg/gateway/handlers/sqlite/backup_handler.go | 13 ++- pkg/gateway/handlers/sqlite/create_handler.go | 18 +++- pkg/gateway/handlers/sqlite/query_handler.go | 17 +++- pkg/gateway/middleware.go | 10 +- pkg/ipfs/client.go | 91 ++++++++++++++---- 14 files changed, 335 insertions(+), 69 deletions(-) diff --git a/e2e/env.go b/e2e/env.go index 677aa33..aa19e9f 100644 --- a/e2e/env.go +++ b/e2e/env.go @@ -1009,13 +1009,70 @@ func LoadTestEnv() (*E2ETestEnv, error) { } // LoadTestEnvWithNamespace loads test environment with a specific namespace +// It creates a new API key for the specified namespace to ensure proper isolation func LoadTestEnvWithNamespace(namespace string) (*E2ETestEnv, error) { - env, err := LoadTestEnv() - if err != nil { - return nil, err + gatewayURL := os.Getenv("ORAMA_GATEWAY_URL") + if gatewayURL == "" { + gatewayURL = GetGatewayURL() } - env.Namespace = namespace - return env, nil + + skipCleanup := os.Getenv("ORAMA_SKIP_CLEANUP") == "true" + + // Generate a unique wallet address for this namespace + // Using namespace as part of the wallet address for uniqueness + wallet := fmt.Sprintf("0x%x", []byte(namespace+fmt.Sprintf("%d", time.Now().UnixNano()))) + if len(wallet) < 42 { + wallet = wallet + strings.Repeat("0", 42-len(wallet)) + } + if len(wallet) > 42 { + wallet = wallet[:42] + } + + // Create an API key for this namespace via the simple-key endpoint + reqBody := map[string]string{ + "wallet": wallet, + "namespace": namespace, + } + bodyBytes, _ := json.Marshal(reqBody) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", gatewayURL+"/v1/auth/simple-key", bytes.NewReader(bodyBytes)) + if err != nil { + return nil, fmt.Errorf("failed to create API key request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + client := NewHTTPClient(10 * time.Second) + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to create API key: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API key creation failed with status %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var apiKeyResp map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&apiKeyResp); err != nil { + return nil, fmt.Errorf("failed to decode API key response: %w", err) + } + + apiKey, ok := apiKeyResp["api_key"].(string) + if !ok || apiKey == "" { + return nil, fmt.Errorf("API key not found in response") + } + + return &E2ETestEnv{ + GatewayURL: gatewayURL, + APIKey: apiKey, + Namespace: namespace, + HTTPClient: NewHTTPClient(30 * time.Second), + SkipCleanup: skipCleanup, + }, nil } // CreateTestDeployment creates a test deployment and returns its ID diff --git a/e2e/namespace_isolation_test.go b/e2e/namespace_isolation_test.go index 4e1ecbb..3d636c3 100644 --- a/e2e/namespace_isolation_test.go +++ b/e2e/namespace_isolation_test.go @@ -112,8 +112,11 @@ func TestNamespaceIsolation_Deployments(t *testing.T) { } func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) { - envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) - envB, _ := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) + envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) + require.NoError(t, err, "Should create test environment for namespace-a") + + envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) + require.NoError(t, err, "Should create test environment for namespace-b") // Create database in namespace-a dbNameA := "users-db-a" @@ -198,8 +201,11 @@ func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) { } func TestNamespaceIsolation_IPFSContent(t *testing.T) { - envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) - envB, _ := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) + envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) + require.NoError(t, err, "Should create test environment for namespace-a") + + envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) + require.NoError(t, err, "Should create test environment for namespace-b") // Upload file in namespace-a cidA := UploadTestFile(t, envA, "test-file-a.txt", "Content from namespace A") @@ -294,21 +300,26 @@ func TestNamespaceIsolation_IPFSContent(t *testing.T) { } func TestNamespaceIsolation_OlricCache(t *testing.T) { - envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) - envB, _ := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) + envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) + require.NoError(t, err, "Should create test environment for namespace-a") + envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) + require.NoError(t, err, "Should create test environment for namespace-b") + + dmap := "test-cache" keyA := "user-session-123" valueA := `{"user_id": "alice", "token": "secret-token-a"}` t.Run("Namespace-A sets cache key", func(t *testing.T) { reqBody := map[string]interface{}{ + "dmap": dmap, "key": keyA, "value": valueA, - "ttl": 300, + "ttl": "300s", } bodyBytes, _ := json.Marshal(reqBody) - req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/set", bytes.NewReader(bodyBytes)) + req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/put", bytes.NewReader(bodyBytes)) req.Header.Set("Authorization", "Bearer "+envA.APIKey) req.Header.Set("Content-Type", "application/json") @@ -322,8 +333,15 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) { }) t.Run("Namespace-B cannot GET Namespace-A cache key", func(t *testing.T) { - req, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/cache/get?key="+keyA, nil) + reqBody := map[string]interface{}{ + "dmap": dmap, + "key": keyA, + } + bodyBytes, _ := json.Marshal(reqBody) + + req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes)) req.Header.Set("Authorization", "Bearer "+envB.APIKey) + req.Header.Set("Content-Type", "application/json") resp, err := envB.HTTPClient.Do(req) require.NoError(t, err, "Should execute request") @@ -336,7 +354,10 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) { }) t.Run("Namespace-B cannot DELETE Namespace-A cache key", func(t *testing.T) { - reqBody := map[string]string{"key": keyA} + reqBody := map[string]string{ + "dmap": dmap, + "key": keyA, + } bodyBytes, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/delete", bytes.NewReader(bodyBytes)) @@ -351,8 +372,15 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) { assert.Contains(t, []int{http.StatusOK, http.StatusNotFound}, resp.StatusCode) // Verify key still exists for namespace-a - req2, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/cache/get?key="+keyA, nil) + reqBody2 := map[string]interface{}{ + "dmap": dmap, + "key": keyA, + } + bodyBytes2, _ := json.Marshal(reqBody2) + + req2, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes2)) req2.Header.Set("Authorization", "Bearer "+envA.APIKey) + req2.Header.Set("Content-Type", "application/json") resp2, err := envA.HTTPClient.Do(req2) require.NoError(t, err, "Should execute request") @@ -361,10 +389,13 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) { assert.Equal(t, http.StatusOK, resp2.StatusCode, "Key should still exist in namespace A") var result map[string]interface{} - bodyBytes2, _ := io.ReadAll(resp2.Body) - require.NoError(t, json.Unmarshal(bodyBytes2, &result), "Should decode result") + bodyBytes3, _ := io.ReadAll(resp2.Body) + require.NoError(t, json.Unmarshal(bodyBytes3, &result), "Should decode result") - assert.Equal(t, valueA, result["value"], "Value should match") + // Parse expected JSON string for comparison + var expectedValue map[string]interface{} + json.Unmarshal([]byte(valueA), &expectedValue) + assert.Equal(t, expectedValue, result["value"], "Value should match") t.Logf("✓ Namespace B cannot DELETE Namespace A cache key") }) @@ -374,13 +405,14 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) { valueB := `{"user_id": "bob", "token": "secret-token-b"}` reqBody := map[string]interface{}{ + "dmap": dmap, "key": keyA, // Same key name as namespace-a "value": valueB, - "ttl": 300, + "ttl": "300s", } bodyBytes, _ := json.Marshal(reqBody) - req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/set", bytes.NewReader(bodyBytes)) + req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/put", bytes.NewReader(bodyBytes)) req.Header.Set("Authorization", "Bearer "+envB.APIKey) req.Header.Set("Content-Type", "application/json") @@ -391,8 +423,15 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode, "Should set key in namespace B") // Verify namespace-a still has their value - req2, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/cache/get?key="+keyA, nil) + reqBody2 := map[string]interface{}{ + "dmap": dmap, + "key": keyA, + } + bodyBytes2, _ := json.Marshal(reqBody2) + + req2, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes2)) req2.Header.Set("Authorization", "Bearer "+envA.APIKey) + req2.Header.Set("Content-Type", "application/json") resp2, _ := envA.HTTPClient.Do(req2) defer resp2.Body.Close() @@ -401,11 +440,21 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) { bodyBytesA, _ := io.ReadAll(resp2.Body) require.NoError(t, json.Unmarshal(bodyBytesA, &resultA), "Should decode result A") - assert.Equal(t, valueA, resultA["value"], "Namespace A value should be unchanged") + // Parse expected JSON string for comparison + var expectedValueA map[string]interface{} + json.Unmarshal([]byte(valueA), &expectedValueA) + assert.Equal(t, expectedValueA, resultA["value"], "Namespace A value should be unchanged") // Verify namespace-b has their different value - req3, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/cache/get?key="+keyA, nil) + reqBody3 := map[string]interface{}{ + "dmap": dmap, + "key": keyA, + } + bodyBytes3, _ := json.Marshal(reqBody3) + + req3, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes3)) req3.Header.Set("Authorization", "Bearer "+envB.APIKey) + req3.Header.Set("Content-Type", "application/json") resp3, _ := envB.HTTPClient.Do(req3) defer resp3.Body.Close() @@ -414,7 +463,10 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) { bodyBytesB, _ := io.ReadAll(resp3.Body) require.NoError(t, json.Unmarshal(bodyBytesB, &resultB), "Should decode result B") - assert.Equal(t, valueB, resultB["value"], "Namespace B value should be different") + // Parse expected JSON string for comparison + var expectedValueB map[string]interface{} + json.Unmarshal([]byte(valueB), &expectedValueB) + assert.Equal(t, expectedValueB, resultB["value"], "Namespace B value should be different") t.Logf("✓ Namespace B can set same key name independently") t.Logf(" - Namespace A value: %s", valueA) diff --git a/pkg/auth/simple_auth.go b/pkg/auth/simple_auth.go index 597e84f..1ae21b2 100644 --- a/pkg/auth/simple_auth.go +++ b/pkg/auth/simple_auth.go @@ -43,16 +43,20 @@ func PerformSimpleAuthentication(gatewayURL string) (*Credentials, error) { return nil, fmt.Errorf("invalid wallet address format") } - // Read namespace (optional) - fmt.Print("Enter namespace (press Enter for 'default'): ") - nsInput, err := reader.ReadString('\n') - if err != nil { - return nil, fmt.Errorf("failed to read namespace: %w", err) - } + // Read namespace (required) + var namespace string + for { + fmt.Print("Enter namespace (required): ") + nsInput, err := reader.ReadString('\n') + if err != nil { + return nil, fmt.Errorf("failed to read namespace: %w", err) + } - namespace := strings.TrimSpace(nsInput) - if namespace == "" { - namespace = "default" + namespace = strings.TrimSpace(nsInput) + if namespace != "" { + break + } + fmt.Println("⚠️ Namespace cannot be empty. Please enter a namespace.") } fmt.Printf("\n✅ Wallet: %s\n", wallet) diff --git a/pkg/gateway/dependencies.go b/pkg/gateway/dependencies.go index 8800b6d..b728d55 100644 --- a/pkg/gateway/dependencies.go +++ b/pkg/gateway/dependencies.go @@ -283,6 +283,7 @@ func initializeIPFS(logger *logging.ColoredLogger, cfg *Config, deps *Dependenci ipfsCfg := ipfs.Config{ ClusterAPIURL: ipfsClusterURL, + IPFSAPIURL: ipfsAPIURL, Timeout: ipfsTimeout, } diff --git a/pkg/gateway/handlers/cache/delete_handler.go b/pkg/gateway/handlers/cache/delete_handler.go index a0fe5dc..d772d35 100644 --- a/pkg/gateway/handlers/cache/delete_handler.go +++ b/pkg/gateway/handlers/cache/delete_handler.go @@ -55,8 +55,16 @@ func (h *CacheHandlers) DeleteHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() + // Namespace isolation: prefix dmap with namespace + namespace := getNamespaceFromContext(ctx) + if namespace == "" { + writeError(w, http.StatusUnauthorized, "namespace not found in context") + return + } + namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap) + olricCluster := h.olricClient.GetClient() - dm, err := olricCluster.NewDMap(req.DMap) + dm, err := olricCluster.NewDMap(namespacedDMap) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) return diff --git a/pkg/gateway/handlers/cache/get_handler.go b/pkg/gateway/handlers/cache/get_handler.go index 4c3f564..228ef48 100644 --- a/pkg/gateway/handlers/cache/get_handler.go +++ b/pkg/gateway/handlers/cache/get_handler.go @@ -57,8 +57,16 @@ func (h *CacheHandlers) GetHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() + // Namespace isolation: prefix dmap with namespace + namespace := getNamespaceFromContext(ctx) + if namespace == "" { + writeError(w, http.StatusUnauthorized, "namespace not found in context") + return + } + namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap) + olricCluster := h.olricClient.GetClient() - dm, err := olricCluster.NewDMap(req.DMap) + dm, err := olricCluster.NewDMap(namespacedDMap) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) return @@ -146,8 +154,16 @@ func (h *CacheHandlers) MultiGetHandler(w http.ResponseWriter, r *http.Request) ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() + // Namespace isolation: prefix dmap with namespace + namespace := getNamespaceFromContext(ctx) + if namespace == "" { + writeError(w, http.StatusUnauthorized, "namespace not found in context") + return + } + namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap) + olricCluster := h.olricClient.GetClient() - dm, err := olricCluster.NewDMap(req.DMap) + dm, err := olricCluster.NewDMap(namespacedDMap) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) return diff --git a/pkg/gateway/handlers/cache/list_handler.go b/pkg/gateway/handlers/cache/list_handler.go index 4d0d956..6d85bb3 100644 --- a/pkg/gateway/handlers/cache/list_handler.go +++ b/pkg/gateway/handlers/cache/list_handler.go @@ -54,8 +54,16 @@ func (h *CacheHandlers) ScanHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() + // Namespace isolation: prefix dmap with namespace + namespace := getNamespaceFromContext(ctx) + if namespace == "" { + writeError(w, http.StatusUnauthorized, "namespace not found in context") + return + } + namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap) + olricCluster := h.olricClient.GetClient() - dm, err := olricCluster.NewDMap(req.DMap) + dm, err := olricCluster.NewDMap(namespacedDMap) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) return diff --git a/pkg/gateway/handlers/cache/set_handler.go b/pkg/gateway/handlers/cache/set_handler.go index 4289afe..0e08a0d 100644 --- a/pkg/gateway/handlers/cache/set_handler.go +++ b/pkg/gateway/handlers/cache/set_handler.go @@ -7,8 +7,18 @@ import ( "net/http" "strings" "time" + + "github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys" ) +// getNamespaceFromContext extracts the namespace from the request context +func getNamespaceFromContext(ctx context.Context) string { + if ns, ok := ctx.Value(ctxkeys.NamespaceOverride).(string); ok { + return ns + } + return "" +} + // SetHandler handles cache PUT/SET requests for storing a key-value pair in a distributed map. // It expects a JSON body with "dmap", "key", and "value" fields, and optionally "ttl". // The value can be any JSON-serializable type (string, number, object, array, etc.). @@ -60,8 +70,16 @@ func (h *CacheHandlers) SetHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() + // Namespace isolation: prefix dmap with namespace + namespace := getNamespaceFromContext(ctx) + if namespace == "" { + writeError(w, http.StatusUnauthorized, "namespace not found in context") + return + } + namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap) + olricCluster := h.olricClient.GetClient() - dm, err := olricCluster.NewDMap(req.DMap) + dm, err := olricCluster.NewDMap(namespacedDMap) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) return diff --git a/pkg/gateway/handlers/deployments/static_handler.go b/pkg/gateway/handlers/deployments/static_handler.go index 5feb87b..a57b5cd 100644 --- a/pkg/gateway/handlers/deployments/static_handler.go +++ b/pkg/gateway/handlers/deployments/static_handler.go @@ -92,6 +92,7 @@ func (h *StaticDeploymentHandler) HandleUpload(w http.ResponseWriter, r *http.Re ) // Extract tarball to temporary directory + // Create a wrapper directory so IPFS creates a root CID tmpDir, err := os.MkdirTemp("", "static-deploy-*") if err != nil { h.logger.Error("Failed to create temp directory", zap.Error(err)) @@ -100,13 +101,21 @@ func (h *StaticDeploymentHandler) HandleUpload(w http.ResponseWriter, r *http.Re } defer os.RemoveAll(tmpDir) - if err := extractTarball(file, tmpDir); err != nil { + // Extract into a subdirectory called "site" so we get a root directory CID + siteDir := filepath.Join(tmpDir, "site") + if err := os.MkdirAll(siteDir, 0755); err != nil { + h.logger.Error("Failed to create site directory", zap.Error(err)) + http.Error(w, "Failed to process tarball", http.StatusInternalServerError) + return + } + + if err := extractTarball(file, siteDir); err != nil { h.logger.Error("Failed to extract tarball", zap.Error(err)) http.Error(w, "Failed to extract tarball", http.StatusInternalServerError) return } - // Upload extracted directory to IPFS + // Upload the parent directory (tmpDir) to IPFS, which will create a CID for the "site" subdirectory addResp, err := h.ipfsClient.AddDirectory(ctx, tmpDir) if err != nil { h.logger.Error("Failed to upload to IPFS", zap.Error(err)) diff --git a/pkg/gateway/handlers/sqlite/backup_handler.go b/pkg/gateway/handlers/sqlite/backup_handler.go index 02321d4..57681a3 100644 --- a/pkg/gateway/handlers/sqlite/backup_handler.go +++ b/pkg/gateway/handlers/sqlite/backup_handler.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys" "github.com/DeBrosOfficial/network/pkg/ipfs" "go.uber.org/zap" ) @@ -30,7 +31,11 @@ func NewBackupHandler(sqliteHandler *SQLiteHandler, ipfsClient ipfs.IPFSClient, // BackupDatabase backs up a database to IPFS func (h *BackupHandler) BackupDatabase(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - namespace := ctx.Value("namespace").(string) + namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string) + if !ok || namespace == "" { + http.Error(w, "Namespace not found in context", http.StatusUnauthorized) + return + } var req struct { DatabaseName string `json:"database_name"` @@ -137,7 +142,11 @@ func (h *BackupHandler) recordBackup(ctx context.Context, dbID, cid string) { // ListBackups lists all backups for a database func (h *BackupHandler) ListBackups(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - namespace := ctx.Value("namespace").(string) + namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string) + if !ok || namespace == "" { + http.Error(w, "Namespace not found in context", http.StatusUnauthorized) + return + } databaseName := r.URL.Query().Get("database_name") if databaseName == "" { diff --git a/pkg/gateway/handlers/sqlite/create_handler.go b/pkg/gateway/handlers/sqlite/create_handler.go index dce2751..274b78b 100644 --- a/pkg/gateway/handlers/sqlite/create_handler.go +++ b/pkg/gateway/handlers/sqlite/create_handler.go @@ -11,6 +11,7 @@ import ( "time" "github.com/DeBrosOfficial/network/pkg/deployments" + "github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys" "github.com/DeBrosOfficial/network/pkg/rqlite" "github.com/google/uuid" "go.uber.org/zap" @@ -27,18 +28,31 @@ type SQLiteHandler struct { // NewSQLiteHandler creates a new SQLite handler func NewSQLiteHandler(db rqlite.Client, homeNodeManager *deployments.HomeNodeManager, logger *zap.Logger) *SQLiteHandler { + // Use user's home directory for cross-platform compatibility + homeDir, err := os.UserHomeDir() + if err != nil { + logger.Error("Failed to get user home directory", zap.Error(err)) + homeDir = os.Getenv("HOME") + } + + basePath := filepath.Join(homeDir, ".orama", "sqlite") + return &SQLiteHandler{ db: db, homeNodeManager: homeNodeManager, logger: logger, - basePath: "/home/debros/.orama/data/sqlite", + basePath: basePath, } } // CreateDatabase creates a new SQLite database for a namespace func (h *SQLiteHandler) CreateDatabase(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - namespace := ctx.Value("namespace").(string) + namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string) + if !ok || namespace == "" { + http.Error(w, "Namespace not found in context", http.StatusUnauthorized) + return + } var req struct { DatabaseName string `json:"database_name"` diff --git a/pkg/gateway/handlers/sqlite/query_handler.go b/pkg/gateway/handlers/sqlite/query_handler.go index 883fa78..2c0904c 100644 --- a/pkg/gateway/handlers/sqlite/query_handler.go +++ b/pkg/gateway/handlers/sqlite/query_handler.go @@ -1,12 +1,14 @@ package sqlite import ( + "context" "database/sql" "encoding/json" "net/http" "os" "strings" + "github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys" "go.uber.org/zap" ) @@ -29,7 +31,11 @@ type QueryResponse struct { // QueryDatabase executes a SQL query on a namespace database func (h *SQLiteHandler) QueryDatabase(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - namespace := ctx.Value("namespace").(string) + namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string) + if !ok || namespace == "" { + http.Error(w, "Namespace not found in context", http.StatusUnauthorized) + return + } var req QueryRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -179,7 +185,8 @@ func (h *SQLiteHandler) updateDatabaseSize(namespace, databaseName, filePath str } query := `UPDATE namespace_sqlite_databases SET size_bytes = ? WHERE namespace = ? AND database_name = ?` - _, err = h.db.Exec(nil, query, stat.Size(), namespace, databaseName) + ctx := context.Background() + _, err = h.db.Exec(ctx, query, stat.Size(), namespace, databaseName) if err != nil { h.logger.Error("Failed to update database size", zap.Error(err)) } @@ -199,7 +206,11 @@ type DatabaseInfo struct { // ListDatabases lists all databases for a namespace func (h *SQLiteHandler) ListDatabases(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - namespace := ctx.Value("namespace").(string) + namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string) + if !ok || namespace == "" { + http.Error(w, "Namespace not found in context", http.StatusUnauthorized) + return + } var databases []DatabaseInfo query := ` diff --git a/pkg/gateway/middleware.go b/pkg/gateway/middleware.go index 2f0f67c..05dc824 100644 --- a/pkg/gateway/middleware.go +++ b/pkg/gateway/middleware.go @@ -459,9 +459,13 @@ func (g *Gateway) domainRoutingMiddleware(next http.Handler) http.Handler { // Try to find deployment by domain deployment, err := g.getDeploymentByDomain(r.Context(), host) - if err != nil || deployment == nil { - // Not a deployment domain, continue to normal routing - next.ServeHTTP(w, r) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + if deployment == nil { + // Domain matches .orama.network but no deployment found + http.NotFound(w, r) return } diff --git a/pkg/ipfs/client.go b/pkg/ipfs/client.go index 3052202..cdeac08 100644 --- a/pkg/ipfs/client.go +++ b/pkg/ipfs/client.go @@ -33,9 +33,10 @@ type IPFSClient interface { // Client wraps an IPFS Cluster HTTP API client for storage operations type Client struct { - apiURL string - httpClient *http.Client - logger *zap.Logger + apiURL string + ipfsAPIURL string + httpClient *http.Client + logger *zap.Logger } // Config holds configuration for the IPFS client @@ -44,6 +45,10 @@ type Config struct { // If empty, defaults to "http://localhost:9094" ClusterAPIURL string + // IPFSAPIURL is the base URL for IPFS daemon API (e.g., "http://localhost:4501") + // Used for operations that require IPFS daemon directly (like directory uploads) + IPFSAPIURL string + // Timeout is the timeout for client operations // If zero, defaults to 60 seconds Timeout time.Duration @@ -68,6 +73,14 @@ type AddResponse struct { Size int64 `json:"size"` } +// ipfsDaemonAddResponse represents the response from IPFS daemon's /add endpoint +// The daemon returns Size as a string, unlike Cluster which returns it as int64 +type ipfsDaemonAddResponse struct { + Name string `json:"Name"` + Hash string `json:"Hash"` // Daemon uses "Hash" instead of "Cid" + Size string `json:"Size"` // Daemon returns size as string +} + // PinResponse represents the response from pinning a CID type PinResponse struct { Cid string `json:"cid"` @@ -81,6 +94,11 @@ func NewClient(cfg Config, logger *zap.Logger) (*Client, error) { apiURL = "http://localhost:9094" } + ipfsAPIURL := cfg.IPFSAPIURL + if ipfsAPIURL == "" { + ipfsAPIURL = "http://localhost:4501" + } + timeout := cfg.Timeout if timeout == 0 { timeout = 60 * time.Second @@ -92,6 +110,7 @@ func NewClient(cfg Config, logger *zap.Logger) (*Client, error) { return &Client{ apiURL: apiURL, + ipfsAPIURL: ipfsAPIURL, httpClient: httpClient, logger: logger, }, nil @@ -240,23 +259,26 @@ func (c *Client) Add(ctx context.Context, reader io.Reader, name string) (*AddRe } // AddDirectory adds all files in a directory to IPFS and returns the root directory CID +// Uses IPFS daemon's multipart upload to preserve directory structure func (c *Client) AddDirectory(ctx context.Context, dirPath string) (*AddResponse, error) { var buf bytes.Buffer writer := multipart.NewWriter(&buf) - // Walk directory and add all files to multipart request var totalSize int64 + var fileCount int + + // Walk directory and add all files to multipart request err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - // Skip directories + // Skip directories themselves (IPFS will create them from file paths) if info.IsDir() { return nil } - // Get relative path + // Get relative path from dirPath relPath, err := filepath.Rel(dirPath, path) if err != nil { return fmt.Errorf("failed to get relative path: %w", err) @@ -269,8 +291,9 @@ func (c *Client) AddDirectory(ctx context.Context, dirPath string) (*AddResponse } totalSize += int64(len(data)) + fileCount++ - // Add file to multipart + // Add file to multipart with relative path part, err := writer.CreateFormFile("file", relPath) if err != nil { return fmt.Errorf("failed to create form file: %w", err) @@ -287,14 +310,19 @@ func (c *Client) AddDirectory(ctx context.Context, dirPath string) (*AddResponse return nil, err } + if fileCount == 0 { + return nil, fmt.Errorf("no files found in directory") + } + if err := writer.Close(); err != nil { return nil, fmt.Errorf("failed to close writer: %w", err) } - // Add with wrap-in-directory to create a root directory node - apiURL := c.apiURL + "/add?wrap-in-directory=true" + // Upload to IPFS daemon (not Cluster) with wrap-in-directory + // This creates a UnixFS directory structure + ipfsDaemonURL := c.ipfsAPIURL + "/api/v0/add?wrap-in-directory=true" - req, err := http.NewRequestWithContext(ctx, "POST", apiURL, &buf) + req, err := http.NewRequestWithContext(ctx, "POST", ipfsDaemonURL, &buf) if err != nil { return nil, fmt.Errorf("failed to create add request: %w", err) } @@ -312,27 +340,53 @@ func (c *Client) AddDirectory(ctx context.Context, dirPath string) (*AddResponse return nil, fmt.Errorf("add failed with status %d: %s", resp.StatusCode, string(body)) } - // Read NDJSON responses - the last one will be the root directory + // Read NDJSON responses + // IPFS daemon returns entries for each file and subdirectory + // The last entry should be the root directory (or deepest subdirectory if no wrapper) dec := json.NewDecoder(resp.Body) - var last AddResponse + var rootCID string + var lastEntry ipfsDaemonAddResponse for { - var chunk AddResponse + var chunk ipfsDaemonAddResponse if err := dec.Decode(&chunk); err != nil { if errors.Is(err, io.EOF) { break } return nil, fmt.Errorf("failed to decode add response: %w", err) } - last = chunk + lastEntry = chunk + + // With wrap-in-directory, the entry with empty name is the wrapper directory + if chunk.Name == "" { + rootCID = chunk.Hash + } } - if last.Cid == "" { - return nil, fmt.Errorf("no CID returned from IPFS") + // Use the last entry if no wrapper directory found + if rootCID == "" { + rootCID = lastEntry.Hash + } + + if rootCID == "" { + return nil, fmt.Errorf("no root CID returned from IPFS daemon") + } + + c.logger.Debug("Directory uploaded to IPFS", + zap.String("root_cid", rootCID), + zap.Int("file_count", fileCount), + zap.Int64("total_size", totalSize)) + + // Pin to cluster for distribution + _, err = c.Pin(ctx, rootCID, "", 1) + if err != nil { + c.logger.Warn("Failed to pin directory to cluster", + zap.String("cid", rootCID), + zap.Error(err)) } return &AddResponse{ - Cid: last.Cid, + Cid: rootCID, Size: totalSize, }, nil } @@ -496,8 +550,9 @@ func (c *Client) Unpin(ctx context.Context, cid string) error { // Get retrieves content from IPFS by CID // Note: This uses the IPFS HTTP API (typically on port 5001), not the Cluster API func (c *Client) Get(ctx context.Context, cid string, ipfsAPIURL string) (io.ReadCloser, error) { + // Use the client's configured IPFS API URL if not provided if ipfsAPIURL == "" { - ipfsAPIURL = "http://localhost:5001" + ipfsAPIURL = c.ipfsAPIURL } url := fmt.Sprintf("%s/api/v0/cat?arg=%s", ipfsAPIURL, cid)