package filesharing import ( "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "path/filepath" app2 "cwtch.im/cwtch/app" "cwtch.im/cwtch/event" "cwtch.im/cwtch/functionality/filesharing" "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/model/constants" "cwtch.im/cwtch/peer" "cwtch.im/cwtch/protocol/connections" "cwtch.im/cwtch/protocol/files" utils2 "cwtch.im/cwtch/utils" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" // Import SQL Cipher mrand "math/rand" "os" "os/user" "path" "runtime" "runtime/pprof" "testing" "time" _ "github.com/mutecomm/go-sqlcipher/v4" ) func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.CwtchPeer) { for { state := peera.GetPeerState(peerb.GetOnion()) if state == connections.FAILED { t.Fatalf("%v could not connect to %v", peera.GetOnion(), peerb.GetOnion()) } if state != connections.AUTHENTICATED { fmt.Printf("peer %v waiting connect to peer %v, currently: %v\n", peera.GetOnion(), peerb.GetOnion(), connections.ConnectionStateName[state]) time.Sleep(time.Second * 5) continue } else { peerAName, _ := peera.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) peerBName, _ := peerb.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName) break } } } func TestFileSharing(t *testing.T) { numGoRoutinesStart := runtime.NumGoroutine() os.RemoveAll("cwtch.out.png") os.RemoveAll("cwtch.out.png.manifest") log.SetLevel(log.LevelDebug) log.ExcludeFromPattern("tapir") os.Mkdir("tordir", 0700) dataDir := path.Join("tordir", "tor") os.MkdirAll(dataDir, 0700) // we don't need real randomness for the port, just to avoid a possible conflict... socksPort := mrand.Intn(1000) + 9051 controlPort := mrand.Intn(1000) + 9052 // generate a random password key := make([]byte, 64) _, err := rand.Read(key) if err != nil { panic(err) } useCache := os.Getenv("TORCACHE") == "true" torDataDir := "" if useCache { log.Infof("using tor cache") torDataDir = filepath.Join(dataDir, "data-dir-torcache") os.MkdirAll(torDataDir, 0700) } else { log.Infof("using clean tor data dir") if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil { t.Fatalf("could not create data dir") } } tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc") acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)}) if err != nil { t.Fatalf("Could not start Tor: %v", err) } acn.WaitTillBootstrapped() defer acn.Close() app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage")) usr, err := user.Current() if err != nil { t.Fatalf("current user is undefined") } cwtchDir := path.Join(usr.HomeDir, ".cwtch") os.Mkdir(cwtchDir, 0700) os.RemoveAll(path.Join(cwtchDir, "testing")) os.Mkdir(path.Join(cwtchDir, "testing"), 0700) t.Logf("Creating Alice...") app.CreateProfile("alice", "asdfasdf", true) t.Logf("Creating Bob...") app.CreateProfile("bob", "asdfasdf", true) t.Logf("** Waiting for Alice, Bob...") alice := app2.WaitGetPeer(app, "alice") app.ActivatePeerEngine(alice.GetOnion()) app.ConfigureConnections(alice.GetOnion(), true, true, true) bob := app2.WaitGetPeer(app, "bob") app.ActivatePeerEngine(bob.GetOnion()) app.ConfigureConnections(bob.GetOnion(), true, true, true) alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) aliceQueueOracle := event.NewQueue() aliceEb := app.GetEventBus(alice.GetOnion()) if aliceEb == nil { t.Fatalf("alice's eventbus is undefined") } aliceEb.Subscribe(event.SearchResult, aliceQueueOracle) queueOracle := event.NewQueue() bobEb := app.GetEventBus(bob.GetOnion()) if bobEb == nil { t.Fatalf("bob's eventbus is undefined") } bobEb.Subscribe(event.FileDownloaded, queueOracle) // Turn on File Sharing Experiment... settings := app.ReadSettings() settings.ExperimentsEnabled = true settings.Experiments[constants.FileSharingExperiment] = true app.UpdateSettings(settings) t.Logf("** Launching Peers...") waitTime := time.Duration(30) * time.Second t.Logf("** Waiting for Alice, Bob to connect with onion network... (%v)\n", waitTime) time.Sleep(waitTime) bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true) alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true) filesharingFunctionality := filesharing.FunctionalityGate() _, fileSharingMessage, err := filesharingFunctionality.ShareFile("cwtch.png", alice) if err != nil { t.Fatalf("Error!: %v", err) } alice.SendMessage(1, fileSharingMessage) // Ok this is fun...we just Sent a Message we may not have a connection yet... // so this test will only pass if sending offline works... waitForPeerPeerConnection(t, bob, alice) bob.SendMessage(1, "this is a test message") bob.SendMessage(1, "this is another test message") // Wait for the messages to arrive... time.Sleep(time.Second * 20) alice.SearchConversations("test") results := 0 for { ev := aliceQueueOracle.Next() if ev.EventType != event.SearchResult { t.Fatalf("Expected a search result vent") } results += 1 t.Logf("found search result (%d)....%v", results, ev) if results == 2 { break } } // test that bob can download and verify the file testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle) // Test stopping and restarting file shares t.Logf("Stopping File Share") filesharingFunctionality.StopAllFileShares(alice) // Allow time for the stop request to filter through Engine time.Sleep(time.Second * 5) // Restart t.Logf("Restarting File Share") err = filesharingFunctionality.ReShareFiles(alice) if err != nil { t.Fatalf("Error!: %v", err) } // run the same download test again...to check that we can actually download the file testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle) // test that we can delete bob... app.DeleteProfile(bob.GetOnion(), "asdfasdf") aliceQueueOracle.Shutdown() queueOracle.Shutdown() app.Shutdown() acn.Close() time.Sleep(5 * time.Second) numGoRoutinesPostACN := runtime.NumGoroutine() // Printing out the current goroutines // Very useful if we are leaking any. pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) if numGoRoutinesStart != numGoRoutinesPostACN { t.Errorf("Number of GoRoutines at start (%v) does not match number of goRoutines after cleanup of peers and servers (%v), clean up failed, leak detected!", numGoRoutinesStart, numGoRoutinesPostACN) } } func testBobDownloadFile(t *testing.T, bob peer.CwtchPeer, filesharingFunctionality *filesharing.Functionality, queueOracle event.Queue) { os.RemoveAll("cwtch.out.png") os.RemoveAll("cwtch.out.png.manifest") message, _, err := bob.GetChannelMessage(1, 0, 1) if err != nil { t.Fatalf("could not find file sharing message: %v", err) } var messageWrapper model.MessageWrapper json.Unmarshal([]byte(message), &messageWrapper) if messageWrapper.Overlay == model.OverlayFileSharing { var fileMessageOverlay filesharing.OverlayMessage err := json.Unmarshal([]byte(messageWrapper.Data), &fileMessageOverlay) if err == nil { t.Logf("bob attempting to download file with invalid download") // try downloading with invalid download dir err = filesharingFunctionality.DownloadFile(bob, 1, "/do/not/download/this/file/cwtch.out.png", "./cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes) if err == nil { t.Fatalf("should not download file with invalid download dir") } t.Logf("bob attempting to download file with invalid manifest") // try downloading with invalid manifest dir err = filesharingFunctionality.DownloadFile(bob, 1, "./cwtch.out.png", "/do/not/download/this/file/cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes) if err == nil { t.Fatalf("should not download file with invalid manifest dir") } t.Logf("bob attempting to download file") err = filesharingFunctionality.DownloadFile(bob, 1, "./cwtch.out.png", "./cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes) if err != nil { t.Fatalf("could not download file: %v", err) } } } // Wait for the file downloaded event ClientTimeout := utils2.TimeoutPolicy(time.Second * 120) err = ClientTimeout.ExecuteAction(func() error { ev := queueOracle.Next() if ev.EventType != event.FileDownloaded { t.Fatalf("Expected file download event") } manifest, _ := files.CreateManifest("cwtch.out.png") if hex.EncodeToString(manifest.RootHash) != "8f0ed73bbb30db45b6a740b1251cae02945f48e4f991464d5f3607685c45dcd136a325dab2e5f6429ce2b715e602b20b5b16bf7438fb6235fefe912adcedb5fd" { t.Fatalf("file hash does not match expected %x: ", manifest.RootHash) } return nil }) if err != nil { t.Fatalf("timeout when attempting to download a file") } }