//package cwtch package main // //Needed to invoke C.free // #include import "C" import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/model/constants" "cwtch.im/cwtch/settings" "encoding/json" "fmt" "git.openprivacy.ca/cwtch.im/cwtch-autobindings/utils" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" "os" "os/user" path "path/filepath" "runtime" "runtime/pprof" "strings" "strconv" "time" mrand "math/rand" "crypto/rand" "encoding/base64" "unsafe" _ "github.com/mutecomm/go-sqlcipher/v4" "cwtch.im/cwtch/app" "git.openprivacy.ca/openprivacy/connectivity" "sync" {{IMPORTS}} ) // supplied by make var ( buildVer string buildDate string ) var application app.Application var globalAppDir string var globalTorPath string var eventHandler *utils.EventHandler var globalACN connectivity.ProxyACN // Dangerous function. Should only be used as documented in `MEMORY.md` // //export c_FreePointer func c_FreePointer(ptr *C.char) { C.free(unsafe.Pointer(ptr)) } //export c_Started func c_Started() C.int { return C.int(Started()) } // Started returns 1 if application is initialized and 0 if it is null func Started() int { if application == nil { return 0 } return 1 } //export c_StartCwtch func c_StartCwtch(dir_c *C.char, len C.int, tor_c *C.char, torLen C.int) C.int { applicationDirectory := C.GoStringN(dir_c, len) torDirectory := C.GoStringN(tor_c, torLen) return C.int(StartCwtch(applicationDirectory, torDirectory)) } // StartCwtch starts cwtch in the library and initlaizes all data structures // // GetAppbusEvents is always safe to use // the rest of functions are unsafe until the CwtchStarted event has been received indicating StartCwtch has completed // returns: // message: CwtchStarted when start up is complete and app is safe to use // CwtchStartError message when start up fails (includes event.Error data field) func StartCwtch(appDir string, torPath string) int { if logfile := os.Getenv("LOG_FILE"); logfile != "" { filelog, err := log.NewFile(log.LevelInfo, logfile) if err == nil { filelog.SetUseColor(false) log.SetStd(filelog) } else { // not so likely to be seen since we're usually creating file log in situations we can't access console logs... log.Errorf("could not create file log: %v\n", err) } } if runtime.GOOS == "android" { log.SetUseColor(false) } log.SetLevel(log.LevelInfo) if logLevel := os.Getenv("LOG_LEVEL"); strings.ToLower(logLevel) == "debug" { log.SetLevel(log.LevelDebug) } log.Infof("StartCwtch(...)") log.Debugf("builddate: %v buildver: %v", buildDate, buildVer) // Quick hack check that we're being called with the correct params // On android a stale worker could be calling us with "last apps" directory. Best to abort fast so the app can make a new worker if runtime.GOOS == "android" { fh, err := os.Open(torPath) if err != nil { log.Errorf("%v", err) log.Errorf("failed to stat tor, skipping StartCwtch(). potentially normal if the app was reinstalled or the device was restarted; this workorder should get canceled soon") return 1 } _ = fh.Close() } go _startCwtch(appDir, torPath) return 0 } func _startCwtch(appDir string, torPath string) { log.Infof("application: %v eventHandler: %v", application, eventHandler) if application != nil { log.Infof("_startCwtch detected existing application; resuming instead of relaunching") ReconnectCwtchForeground() return } log.Infof("Creating new EventHandler()") eventHandler = utils.NewEventHandler() // Exclude Tapir wire Messages //(We need a TRACE level) log.ExcludeFromPattern("service.go") // Environment variables don't get '~' expansion so if CWTCH_DIR was set, it likely needs manual handling usr, _ := user.Current() homeDir := usr.HomeDir if appDir == "~" { appDir = homeDir } else if strings.HasPrefix(appDir, "~/") { appDir = path.Join(homeDir, appDir[2:]) } // Ensure that the application directory exists...and then initialize settings.. err := os.MkdirAll(appDir, 0700) if err != nil { log.Errorf("Error creating appDir %v: %v\n", appDir, err) eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error creating appDir %v: %v", appDir, err))) return } log.Infof("Loading Cwtch Directory %v and tor path: %v", appDir, torPath) settings.InitGlobalSettingsFile(appDir, app.DefactoPasswordForUnencryptedProfiles) log.Infof("making directory %v", appDir) err = os.MkdirAll(path.Join(appDir, "tor"), 0700) if err != nil { log.Errorf("error creating tor data directory: %v. Aborting app start up", err) eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) return } // Allow the user of a custom torrc globalAppDir = appDir globalTorPath = torPath settingsFile := app.LoadAppSettings(appDir) // start with an Error ACN erracn := connectivity.NewErrorACN(fmt.Errorf("initializing tor")) globalACN = connectivity.NewProxyACN(&erracn) application = app.NewApp(&globalACN, appDir, settingsFile) // Subscribe to all App Events... eventHandler.HandleApp(application) // FIXME: This code exists to allow the Splash Screen test in the new UI integration tests to pass // it doesn't actually fix the problem in theory, and we should get around to ensuring that application // is safe to access even if shutdown is called concurrently... if application == nil { log.Errorf("startCwtch: primary application object has gone away. assuming application is closing.") return } // Send global settings to the UI... globalSettings := application.ReadSettings() settingsJson, _ := json.Marshal(globalSettings) application.GetPrimaryBus().Publish(event.NewEvent(settings.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) log.Infof("libcwtch-go application launched") application.GetPrimaryBus().Publish(event.NewEvent(settings.CwtchStarted, map[event.Field]string{})) application.QueryACNVersion() {{EXPERIMENT_REGISTER}} // Finally attempt to set up a proper Tor // Note: ResetTor launches an internal goroutine so this is non-blocking... ResetTor() } // the pointer returned from this function **must** be freed using c_Free // //export c_GetAppBusEvent func c_GetAppBusEvent() *C.char { return C.CString(GetAppBusEvent()) } // GetAppBusEvent blocks until an event func GetAppBusEvent() string { for eventHandler == nil { log.Debugf("waiting for eventHandler != nil") time.Sleep(time.Second) } var json = "" for json == "" { json = eventHandler.GetNextEvent() } return json } //export c_ReconnectCwtchForeground func c_ReconnectCwtchForeground() { ReconnectCwtchForeground() } // Like StartCwtch, but StartCwtch has already been called so we don't need to restart Tor etc (probably) // Do need to re-send initial state tho, eg profiles that are already loaded func ReconnectCwtchForeground() { log.Infof("Reconnecting cwtch foreground") if application == nil { log.Errorf("ReconnectCwtchForeground: Application is nil, presuming stale thread, EXITING Reconnect\n") return } // Repopulate the UI on Android by iterating through all loaded profiles and treating them as New Peer loads. The ReloadEvent field // suppresses any listen/connection actions and simply sends updated attributes to the UI. // The UI is designed in such a way that "NewPeer" events are treated as updates. // TODO: if/when we break apart NewPeer into smaller chunks for the UI to fetch, the need to do this here will go away. peerList := application.ListProfiles() for _, onion := range peerList { eventHandler.Push(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: onion, utils.ReloadEvent: event.True})) } settingsJson, _ := json.Marshal(application.ReadSettings()) application.GetPrimaryBus().Publish(event.NewEvent(settings.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) application.GetPrimaryBus().Publish(event.NewEvent(settings.CwtchStarted, map[event.Field]string{utils.ReloadEvent: event.True})) application.QueryACNStatus() application.QueryACNVersion() } //export c_ShutdownCwtch func c_ShutdownCwtch() { ShutdownCwtch() } // ShutdownCwtch is a safe way to shutdown any active cwtch applications and associated ACNs func ShutdownCwtch() { if application != nil { // Kill the isolate eventHandler.Push(event.NewEvent(event.Shutdown, map[event.Field]string{})) // Allow for the shutdown events to go through and then purge everything else... log.Infof("Shutting Down Application...") application.Shutdown() log.Infof("Shutting Down ACN...") globalACN.Close() log.Infof("Library Shutdown Complete!") // do not remove - important for state checks elsewhere application = nil eventHandler = nil } } // TODO: At some point these functions should also be autogenerated // Attribute is a struct to return the dual values of an attempt at a Get*Attribute API call, meant to be json serialized type Attribute struct { Exists bool Value string } //export c_SetProfileAttribute func c_SetProfileAttribute(profile_ptr *C.char, profile_len C.int, key_ptr *C.char, key_len C.int, val_ptr *C.char, val_len C.int) { profileOnion := C.GoStringN(profile_ptr, profile_len) key := C.GoStringN(key_ptr, key_len) value := C.GoStringN(val_ptr, val_len) SetProfileAttribute(profileOnion, key, value) } // SetProfileAttribute provides a wrapper around profile.SetScopedZonedAttribute // WARNING: Because this function is potentially dangerous all keys and zones must be added // explicitly. If you are attempting to added behaviour to the UI that requires the existence of new keys // you probably want to be building out functionality/subsystem in the UI itself. // Key must have the format zone.key where Zone is defined in Cwtch. Unknown zones are not permitted. func SetProfileAttribute(profileOnion string, key string, value string) { profile := application.GetPeer(profileOnion) if profile != nil { zone, key := attr.ParseZone(key) // TODO We only allow certain public.profile.key to be set for now. // All other scopes and zones need to be added explicitly or handled by Cwtch. if zone == attr.ProfileZone && key == constants.Name { profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, value) } else if zone == attr.ProfileZone && key == constants.ProfileAttribute1 { profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.ProfileAttribute1, value) } else if zone == attr.ProfileZone && key == constants.ProfileAttribute2 { profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.ProfileAttribute2, value) } else if zone == attr.ProfileZone && key == constants.ProfileAttribute3 { profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.ProfileAttribute3, value) } else if zone == attr.ProfileZone && key == constants.ProfileStatus { profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.ProfileStatus, value) } else if zone == attr.ProfileZone && key == constants.PeerAutostart { profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart, value) } else if zone == attr.ProfileZone && key == constants.PeerAppearOffline { profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAppearOffline, value) } else { log.Errorf("attempted to set an attribute with an unknown zone: %v", key) } } } //export c_GetProfileAttribute func c_GetProfileAttribute(profile_ptr *C.char, profile_len C.int, key_ptr *C.char, key_len C.int) *C.char { profileOnion := C.GoStringN(profile_ptr, profile_len) key := C.GoStringN(key_ptr, key_len) return C.CString(GetProfileAttribute(profileOnion, key)) } // GetProfileAttribute provides a wrapper around profile.GetScopedZonedAttribute // Key must have the format zone.key where Zone is defined in Cwtch. Unknown zones are not permitted. // Currently forcing the Public Scope // Returns json of Attribute func GetProfileAttribute(profileOnion string, key string) string { profile := application.GetPeer(profileOnion) if profile != nil { zone, key := attr.ParseZone(key) res, exists := profile.GetScopedZonedAttribute(attr.PublicScope, zone, key) attr := Attribute{exists, res} json, _ := json.Marshal(attr) return string(json) } empty := Attribute{false, ""} json, _ := json.Marshal(empty) return (string(json)) } //export c_SetConversationAttribute func c_SetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, key_ptr *C.char, key_len C.int, val_ptr *C.char, val_len C.int) { profileOnion := C.GoStringN(profile_ptr, profile_len) key := C.GoStringN(key_ptr, key_len) value := C.GoStringN(val_ptr, val_len) SetConversationAttribute(profileOnion, int(conversation_id), key, value) } // SetConversationAttribute provides a wrapper around profile.SetProfileAttribute // key is of format Zone.Key, and the API forces the Local Scope func SetConversationAttribute(profileOnion string, conversationID int, key string, value string) { profile := application.GetPeer(profileOnion) zone, key := attr.ParseZone(key) profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)), value) } //export c_GetConversationAttribute func c_GetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, key_ptr *C.char, key_len C.int) *C.char { profileOnion := C.GoStringN(profile_ptr, profile_len) key := C.GoStringN(key_ptr, key_len) return C.CString(GetConversationAttribute(profileOnion, int(conversation_id), key)) } // GetGonversationAttribute provides a wrapper around profile.GetGonversationAttribute // key is of format Scope.Zone.Key // Returns json of an Attribute func GetConversationAttribute(profileOnion string, conversationID int, key string) string { profile := application.GetPeer(profileOnion) if profile != nil { scope, zonekey := attr.ParseScope(key) zone, key := attr.ParseZone(zonekey) res, err := profile.GetConversationAttribute(conversationID, scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))) attr := Attribute{err == nil, res} json, _ := json.Marshal(attr) return string(json) } empty := Attribute{false, ""} json, _ := json.Marshal(empty) return (string(json)) } //export c_ResetTor func c_ResetTor() { ResetTor() } var torLock sync.Mutex func ResetTor() { go func() { // prevent concurrent calls to this method... torLock.Lock() defer torLock.Unlock() log.Infof("Replacing ACN with new Tor...") settings := application.ReadSettings() globalACN.Close() // we need to close first if dateDir is the same, otherwise buildACN can't launch tor. newAcn, settings, err := buildACN(settings, globalTorPath, globalAppDir) // only update settings if successful. if err == nil { // Only update Tor specific settings... currentSettings := application.ReadSettings() currentSettings.TorCacheDir = settings.TorCacheDir currentSettings.CustomControlPort = settings.CustomControlPort currentSettings.CustomSocksPort = settings.CustomSocksPort currentSettings.CustomTorrc = settings.CustomTorrc application.UpdateSettings(currentSettings) // We need to update settings on reset as buildACN can alter settings, otherwise the next reset will be broken... settings = application.ReadSettings() settingsJson, _ := json.Marshal(settings) application.GetPrimaryBus().Publish(event.NewEvent(UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) } // replace ACN regardlesss globalACN.ReplaceACN(newAcn) application.QueryACNStatus() application.QueryACNVersion() log.Infof("Restarted") }() } const ( CwtchStarted = event.Type("CwtchStarted") CwtchStartError = event.Type("CwtchStartError") UpdateGlobalSettings = event.Type("UpdateGlobalSettings") ) func buildACN(globalSettings settings.GlobalSettings, torPath string, appDir string) (connectivity.ACN, settings.GlobalSettings, error) { socksPort := mrand.Intn(1000) + 9600 controlPort := socksPort + 1 // generate a random password (actually random, stored in memory, for the control port) key := make([]byte, 64) _, err := rand.Read(key) if err != nil { panic(err) } log.Infof("making directory %v", appDir) err = os.MkdirAll(path.Join(appDir, "tor"), 0700) if err != nil { log.Errorf("error creating tor data directory: %v. Aborting app start up", err) eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) erracn := connectivity.NewErrorACN(err) return &erracn, globalSettings, err } if globalSettings.AllowAdvancedTorConfig { controlPort = globalSettings.CustomControlPort socksPort = globalSettings.CustomSocksPort } // Override Ports if on Tails... if cwtchTails := os.Getenv("CWTCH_TAILS"); strings.ToLower(cwtchTails) == "true" { log.Infof("CWTCH_TAILS environment variable set... overriding tor config...") controlPort = 9051 // In tails 5.13 the control port was changed to 951 // so read the Tails Version File and if it exists... b, err := os.ReadFile("/etc/amnesia/version") if err == nil { // the file should start with the version followed // by a space... versionEnd := strings.Index(string(b), " ") versionStr := string(b)[:versionEnd] version, err := strconv.ParseFloat(versionStr, 64) if err == nil { log.Infof("Confirming Tails Version: %v", version) // assert the control port if we are at the dedicated version... // we know this change happened sometime after 5.11 if version >= 5.13 { controlPort = 951 } } else { log.Errorf("Unable to confirm Tails version. CWTCH_TAILS options may not function correctly.") } } socksPort = 9050 globalSettings.CustomControlPort = controlPort globalSettings.CustomSocksPort = socksPort globalSettings.AllowAdvancedTorConfig = true } torrc := tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithControlPort(controlPort).WithHashedPassword(base64.StdEncoding.EncodeToString(key)) // torrc.WithLog(path.Join(appDir, "tor", "tor.log"), tor.TorLogLevelNotice) if globalSettings.UseCustomTorrc { customTorrc := globalSettings.CustomTorrc torrc.WithCustom(strings.Split(customTorrc, "\n")) } else { // Fallback to showing the freshly generated torrc for this session. globalSettings.CustomTorrc = torrc.Preview() globalSettings.CustomControlPort = controlPort globalSettings.CustomSocksPort = socksPort } err = torrc.Build(path.Join(appDir, "tor", "torrc")) if err != nil { log.Errorf("error constructing torrc: %v", err) eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) erracn := connectivity.NewErrorACN(err) return &erracn, globalSettings, err } dataDir := globalSettings.TorCacheDir if !globalSettings.UseTorCache { // purge data dir directories if we are not using them for a cache torDir := path.Join(appDir, "tor") files, err := path.Glob(path.Join(torDir, "data-dir-*")) if err != nil { log.Errorf("could not construct filesystem glob: %v", err) } for _, f := range files { if err := os.RemoveAll(f); err != nil { log.Errorf("could not remove data-dir: %v", err) } } if dataDir, err = os.MkdirTemp(torDir, "data-dir-"); err != nil { eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) erracn := connectivity.NewErrorACN(err) return &erracn, globalSettings, err } } // Persist Current Data Dir as Tor Cache... globalSettings.TorCacheDir = dataDir acn, err := tor.NewTorACNWithAuth(appDir, torPath, dataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)}) if err != nil { log.Errorf("Error connecting to Tor replacing with ErrorACN: %v\n", err) eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) erracn := connectivity.NewErrorACN(err) acn = &erracn } return acn, globalSettings, err } //export c_UpdateSettings func c_UpdateSettings(json_ptr *C.char, json_len C.int) { settingsJson := C.GoStringN(json_ptr, json_len) UpdateSettings(settingsJson) } func UpdateSettings(settingsJson string) { var newSettings settings.GlobalSettings json.Unmarshal([]byte(settingsJson), &newSettings) application.UpdateSettings(newSettings) {{EXPERIMENT_UPDATESETTINGS}} } //export c_GetDebugInfo func c_GetDebugInfo() *C.char { return C.CString(GetDebugInfo()) } type DebugInfo struct { BuildVersion string BuildDate string HeapAllocated float64 HeapInUse float64 HeapReleased float64 HeapObjects uint64 NumThreads uint64 SystemMemory float64 } func GetDebugInfo() string { var memstats runtime.MemStats runtime.ReadMemStats(&memstats) const MegaByte = 1024.0 * 1024.0 debugInfo := new(DebugInfo) debugInfo.BuildVersion = buildVer debugInfo.BuildDate = buildDate debugInfo.HeapAllocated = float64(memstats.HeapAlloc) / MegaByte debugInfo.HeapObjects = memstats.HeapObjects debugInfo.NumThreads = uint64(runtime.NumGoroutine()) debugInfo.HeapReleased = float64(memstats.HeapReleased) / MegaByte debugInfo.HeapInUse = float64(memstats.HeapInuse) / MegaByte debugInfo.SystemMemory = float64(memstats.Sys) / MegaByte if os.Getenv("CWTCH_PROFILE") == "1" { pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) f, _ := os.Create("mem.prof") pprof.WriteHeapProfile(f) } data, _ := json.Marshal(debugInfo) return string(data) } {{BINDINGS}} // Leave as is, needed by ffi func main() {}