//package cwtch package main // //Needed to invoke C.free // #include import "C" import ( "crypto/rand" constants2 "cwtch.im/cwtch/model/constants" "cwtch.im/cwtch/protocol/files" "encoding/json" "fmt" "io/ioutil" path "path/filepath" "runtime/pprof" "git.openprivacy.ca/cwtch.im/libcwtch-go/features" // Import SQL Cipher "os/user" "runtime" "strings" "unsafe" _ "github.com/mutecomm/go-sqlcipher/v4" "cwtch.im/cwtch/app" "cwtch.im/cwtch/event" "cwtch.im/cwtch/functionality/filesharing" "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" "git.openprivacy.ca/cwtch.im/libcwtch-go/constants" "git.openprivacy.ca/cwtch.im/libcwtch-go/features/groups" "git.openprivacy.ca/cwtch.im/libcwtch-go/features/servers" "git.openprivacy.ca/cwtch.im/server" "git.openprivacy.ca/openprivacy/connectivity" "git.openprivacy.ca/cwtch.im/libcwtch-go/utils" "encoding/base64" mrand "math/rand" "os" "time" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" ) const ( // ProfileOnion is an event field that contains the handle for a given profile. // todo: this should probably be moved back into Cwtch, and renamed ProfileHandle (onions are too tor-specific) ProfileOnion = event.Field("ProfileOnion") ) // 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 // ChatMessage API currently not officially documented, see // https://git.openprivacy.ca/cwtch.im/secure-development-handbook/issues/3 // for latest updates for now // // A ChatMessage is the application-layer Cwtch message, delivered to the UI // as serialized json. type ChatMessage struct { O int `json:"o"` D string `json:"d"` } //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(utils.CwtchStartError, event.Error, fmt.Sprintf("Error creating appDir %v: %v", appDir, err))) return } err = utils.InitGlobalSettingsFile(appDir, constants.DefactoPasswordForUnencryptedProfiles) if err != nil { log.Errorf("error initializing global settings file %. Global settings might not be loaded or saves", err) } log.Infof("Loading Cwtch Directory %v and tor path: %v", appDir, torPath) 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(utils.CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) return } // Allow the user of a custom torrc settings := utils.ReadGlobalSettings() globalAppDir = appDir globalTorPath = torPath globalACN = connectivity.NewProxyACN(buildACN(*settings, globalTorPath, globalAppDir)) application = app.NewApp(&globalACN, appDir) servers.InitServers(&globalACN, appDir, eventHandler.Push) eventHandler.HandleApp(application) // Settings may have changed... settings = utils.ReadGlobalSettings() settingsJson, _ := json.Marshal(settings) application.LoadProfiles(constants.DefactoPasswordForUnencryptedProfiles) LoadServers(constants.DefactoPasswordForUnencryptedProfiles) serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { serversHandler.Enable() } publishLoadedServers() LaunchServers() // 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... application.GetPrimaryBus().Publish(event.NewEvent(utils.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) log.Infof("libcwtch-go application launched") application.GetPrimaryBus().Publish(event.NewEvent(utils.CwtchStarted, map[event.Field]string{})) application.QueryACNVersion() } func buildACN(settings utils.GlobalSettings, torPath string, appDir string) connectivity.ACN { mrand.Seed(int64(time.Now().Nanosecond())) 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(utils.CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) return &connectivity.ErrorACN{} } if settings.AllowAdvancedTorConfig { controlPort = settings.CustomControlPort socksPort = settings.CustomSocksPort } torrc := tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithControlPort(controlPort).WithHashedPassword(base64.StdEncoding.EncodeToString(key)) if settings.UseCustomTorrc { customTorrc := settings.CustomTorrc torrc.WithCustom(strings.Split(customTorrc, "\n")) } else { // Fallback to showing the freshly generated torrc for this session. settings.CustomTorrc = torrc.Preview() settings.CustomControlPort = controlPort settings.CustomSocksPort = socksPort utils.WriteGlobalSettings(settings) } err = torrc.Build(path.Join(appDir, "tor", "torrc")) if err != nil { log.Errorf("error constructing torrc: %v", err) eventHandler.Push(event.NewEventList(utils.CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) return &connectivity.ErrorACN{} } dataDir := settings.TorCacheDir if !settings.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 = ioutil.TempDir(torDir, "data-dir-"); err != nil { eventHandler.Push(event.NewEventList(utils.CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) return &connectivity.ErrorACN{} } } // Persist Current Data Dir as Tor Cache... settings.TorCacheDir = dataDir utils.WriteGlobalSettings(settings) 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(utils.CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) acn = &connectivity.ErrorACN{} } return acn } //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_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 } // populate profile list peerList := application.ListProfiles() for _, onion := range peerList { eventHandler.Push(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: onion, "Reload": event.True})) } settings := utils.ReadGlobalSettings() groupHandler, _ := groups.ExperimentGate(settings.Experiments) for _, profileOnion := range peerList { // fix peerpeercontact message counts profile := application.GetPeer(profileOnion) // Group Experiment Server Refresh if groupHandler != nil { serverListForOnion := groupHandler.GetServerInfoList(profile) serversListBytes, _ := json.Marshal(serverListForOnion) eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)})) } } LoadServers(constants.DefactoPasswordForUnencryptedProfiles) publishLoadedServers() settingsJson, _ := json.Marshal(settings) application.GetPrimaryBus().Publish(event.NewEvent(utils.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) application.GetPrimaryBus().Publish(event.NewEvent(utils.CwtchStarted, map[event.Field]string{})) application.QueryACNStatus() application.QueryACNVersion() } func publishLoadedServers() { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { serversList := serversHandler.ListServers() for _, server := range serversList { serverInfo := serversHandler.GetServerInfo(server) ev := event.NewEvent(servers.NewServer, make(map[event.Field]string)) serverInfo.EnrichEvent(&ev) application.GetPrimaryBus().Publish(ev) } } } //export c_SendAppEvent // A generic method for Rebroadcasting App Events from a UI func c_SendAppEvent(json_ptr *C.char, json_len C.int) { eventJson := C.GoStringN(json_ptr, json_len) SendAppEvent(eventJson) } // SendAppEvent is a generic method for Rebroadcasting App Events from a UI func SendAppEvent(eventJson string) { // Convert the Event Json back to a typed Event Struct, this will make the // rest of the logic nicer. var new_event event.Event json.Unmarshal([]byte(eventJson), &new_event) log.Debugf("Event: %v", new_event.EventType) // We need to update the local cache // Ideally I think this would be pushed back into Cwtch switch new_event.EventType { case utils.UpdateGlobalSettings: var globalSettings utils.GlobalSettings err := json.Unmarshal([]byte(new_event.Data[event.Data]), &globalSettings) if err != nil { log.Errorf("Error Unmarshalling Settings %v [%v]", err, new_event.Data[event.Data]) } log.Debugf("New Settings %v", globalSettings) utils.WriteGlobalSettings(globalSettings) settings := utils.ReadGlobalSettings() sh, err := servers.ExperimentGate(settings.Experiments) if err == nil { servers.InitServers(&globalACN, globalAppDir, eventHandler.Push) LoadServers(constants.DefactoPasswordForUnencryptedProfiles) if !servers.Enabled() { sh.Enable() publishLoadedServers() LaunchServers() } } else { servers.Disable() } // Group Experiment Refresh groupHandler, err := groups.ExperimentGate(settings.Experiments) if err == nil { for _, profileOnion := range application.ListProfiles() { serverListForOnion := groupHandler.GetServerInfoList(application.GetPeer(profileOnion)) serversListBytes, _ := json.Marshal(serverListForOnion) eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)})) } } // Explicitly toggle blocking/unblocking of unknown connections for profiles // that have been loaded. if settings.BlockUnknownConnections { for _, onion := range application.ListProfiles() { application.GetPeer(onion).BlockUnknownConnections() } } else { for _, onion := range application.ListProfiles() { application.GetPeer(onion).AllowUnknownConnections() } } case utils.SetLoggingLevel: _, warn := new_event.Data[utils.Warn] _, err := new_event.Data[utils.Error] _, debug := new_event.Data[utils.Debug] _, info := new_event.Data[utils.Info] // Assign logging level in priority order. The highest logging level wins in the // event of multiple fields. if info { log.SetLevel(log.LevelInfo) } else if warn { log.SetLevel(log.LevelWarn) } else if err { log.SetLevel(log.LevelError) } else if debug { log.SetLevel(log.LevelDebug) } default: // do nothing } } //export c_SendProfileEvent // A generic method for Rebroadcasting Profile Events from a UI func c_SendProfileEvent(onion_ptr *C.char, onion_len C.int, json_ptr *C.char, json_len C.int) { onion := C.GoStringN(onion_ptr, onion_len) eventJson := C.GoStringN(json_ptr, json_len) SendProfileEvent(onion, eventJson) } const ( AddContact = event.Type("AddContact") ImportString = event.Field("ImportString") ) // SendProfileEvent is a generic method for Rebroadcasting Profile Events from a UI // Should generally be used for rapidly prototyping new APIs func SendProfileEvent(onion string, eventJson string) { // Convert the Event Json back to a typed Event Struct, this will make the // rest of the logic nicer. var new_event event.Event json.Unmarshal([]byte(eventJson), &new_event) log.Infof("Event: %v %v", onion, new_event) // Get the correct Peer peer := application.GetPeer(onion) if peer == nil { return } // We need to update the local cache // Ideally I think this would be pushed back into Cwtch switch new_event.EventType { default: // rebroadcast catch all log.Infof("Received Event %v for %v but no libCwtch handler found, relaying the event directly", new_event, onion) application.GetEventBus(onion).Publish(new_event) } } //export c_GetAppBusEvent // the pointer returned from this function **must** be freed using c_Free 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_CreateProfile func c_CreateProfile(nick_ptr *C.char, nick_len C.int, pass_ptr *C.char, pass_len C.int) { CreateProfile(C.GoStringN(nick_ptr, nick_len), C.GoStringN(pass_ptr, pass_len)) } func CreateProfile(nick, pass string) { if pass == constants.DefactoPasswordForUnencryptedProfiles { application.CreateTaggedPeer(nick, pass, constants.ProfileTypeV1DefaultPassword) } else { application.CreateTaggedPeer(nick, pass, constants.ProfileTypeV1Password) } } //export c_LoadProfiles func c_LoadProfiles(passwordPtr *C.char, passwordLen C.int) { LoadProfiles(C.GoStringN(passwordPtr, passwordLen)) } func LoadProfiles(pass string) { application.LoadProfiles(pass) } //export c_AcceptConversation func c_AcceptConversation(profilePtr *C.char, profileLen C.int, conversation_id C.int) { AcceptConversation(C.GoStringN(profilePtr, profileLen), int(conversation_id)) } // AcceptConversation takes in a profileOnion and a handle to either a group or a peer and authorizes the handle // for further action (e.g. messaging / connecting to the server / joining the group etc.) func AcceptConversation(profileOnion string, conversationID int) { profile := application.GetPeer(profileOnion) if profile != nil { profile.AcceptConversation(conversationID) } } //export c_BlockContact func c_BlockContact(profilePtr *C.char, profileLen C.int, conversation_id C.int) { BlockContact(C.GoStringN(profilePtr, profileLen), int(conversation_id)) } func BlockContact(profileOnion string, conversationID int) { profile := application.GetPeer(profileOnion) if profile != nil { profile.BlockConversation(conversationID) } } //export c_UnblockContact func c_UnblockContact(profilePtr *C.char, profileLen C.int, conversation_id C.int) { UnblockContact(C.GoStringN(profilePtr, profileLen), int(conversation_id)) } func UnblockContact(profileOnion string, conversationID int) { profile := application.GetPeer(profileOnion) if profile != nil { profile.UnblockConversation(conversationID) } } //export c_GetMessage // the pointer returned from this function **must** be Freed by c_Free func c_GetMessage(profile_ptr *C.char, profile_len C.int, conversation_id C.int, message_index C.int) *C.char { profile := C.GoStringN(profile_ptr, profile_len) return C.CString(GetMessage(profile, int(conversation_id), int(message_index))) } // EnhancedMessage wraps a Cwtch model.Message with some additional data to reduce calls from the UI. type EnhancedMessage struct { model.Message ID int // the actual ID of the message in the database (not the row number) LocalIndex int // local index in the DB (row #). Can be empty (most calls supply it) but lookup by hash will fill it ContentHash string ContactImage string Attributes map[string]string } func GetMessage(profileOnion string, conversationID int, messageIndex int) string { var message EnhancedMessage // There is an edge case that can happen on Android when the app is shutdown while fetching messages... // The worker threads that are spawned can become activated again when the app is opened attempt to finish their job... // In that case we skip processing and just return the empty message... // Note: This is far less likely to happen now that the UI only requests messages *after* syncing has happened and // these requests complete almost immediately v.s. being stalled for seconds to minutes on large groups. if application != nil { profile := application.GetPeer(profileOnion) if profile != nil { messages, err := profile.GetMostRecentMessages(conversationID, 0, messageIndex, 1) if err == nil && len(messages) == 1 { time, _ := time.Parse(time.RFC3339Nano, messages[0].Attr[constants2.AttrSentTimestamp]) message.Message = model.Message{ Message: messages[0].Body, Acknowledged: messages[0].Attr[constants2.AttrAck] == constants2.True, Error: messages[0].Attr[constants2.AttrErr], PeerID: messages[0].Attr[constants2.AttrAuthor], Timestamp: time, } message.ID = messages[0].ID message.Attributes = messages[0].Attr message.ContactImage = utils.RandomProfileImage(message.PeerID) message.ContentHash = model.CalculateContentHash(messages[0].Attr[constants2.AttrAuthor], messages[0].Body) } } } bytes, _ := json.Marshal(message) return string(bytes) } //export c_GetMessageByID // the pointer returned from this function **must** be Freed by c_Free func c_GetMessageByID(profile_ptr *C.char, profile_len C.int, conversation_id C.int, message_index C.int) *C.char { profile := C.GoStringN(profile_ptr, profile_len) return C.CString(GetMessageByID(profile, int(conversation_id), int(message_index))) } func GetMessageByID(profileOnion string, conversationID int, messageID int) string { var message EnhancedMessage // There is an edge case that can happen on Android when the app is shutdown while fetching messages... // The worker threads that are spawned can become activated again when the app is opened attempt to finish their job... // In that case we skip processing and just return the empty message... // Note: This is far less likely to happen now that the UI only requests messages *after* syncing has happened and // these requests complete almost immediately v.s. being stalled for seconds to minutes on large groups. if application != nil { profile := application.GetPeer(profileOnion) if profile != nil { dbmessage, attr, err := profile.GetChannelMessage(conversationID, 0, messageID) if err == nil { time, _ := time.Parse(time.RFC3339Nano, attr[constants2.AttrSentTimestamp]) message.Message = model.Message{ Message: dbmessage, Acknowledged: attr[constants2.AttrAck] == constants2.True, Error: attr[constants2.AttrErr], PeerID: attr[constants2.AttrAuthor], Timestamp: time, } message.ID = messageID message.Attributes = attr message.ContactImage = utils.RandomProfileImage(message.PeerID) message.ContentHash = model.CalculateContentHash(attr[constants2.AttrAuthor], dbmessage) } } } bytes, _ := json.Marshal(message) return string(bytes) } //export c_GetMessagesByContentHash // the pointer returned from this function **must** be freed by calling c_Free func c_GetMessagesByContentHash(profile_ptr *C.char, profile_len C.int, conversation_id C.int, contenthash_ptr *C.char, contenthash_len C.int) *C.char { profile := C.GoStringN(profile_ptr, profile_len) contentHash := C.GoStringN(contenthash_ptr, contenthash_len) return C.CString(GetMessagesByContentHash(profile, int(conversation_id), contentHash)) } func GetMessagesByContentHash(profileOnion string, handle int, contentHash string) string { var message EnhancedMessage if application != nil { profile := application.GetPeer(profileOnion) if profile != nil { offset, err := profile.GetChannelMessageByContentHash(handle, 0, contentHash) if err == nil { messages, err := profile.GetMostRecentMessages(handle, 0, offset, 1) if err == nil { time, _ := time.Parse(time.RFC3339Nano, messages[0].Attr[constants2.AttrSentTimestamp]) message.Message = model.Message{ Message: messages[0].Body, Acknowledged: messages[0].Attr[constants2.AttrAck] == constants2.True, Error: messages[0].Attr[constants2.AttrErr], PeerID: messages[0].Attr[constants2.AttrAuthor], Timestamp: time, } message.ID = messages[0].ID message.Attributes = messages[0].Attr message.ContactImage = utils.RandomProfileImage(message.PeerID) message.LocalIndex = offset message.ContentHash = contentHash } else { log.Errorf("error fetching local index {} ", err) } } } } bytes, _ := json.Marshal(message) return string(bytes) } //export c_GetMessages // the pointer returned from this function **must** be Freed by c_Free func c_GetMessages(profile_ptr *C.char, profile_len C.int, conversation_id C.int, message_index C.int, count C.int) *C.char { profile := C.GoStringN(profile_ptr, profile_len) return C.CString(GetMessages(profile, int(conversation_id), int(message_index), int(count))) } func GetMessages(profileOnion string, conversationID int, messageIndex int, count int) string { var emessages []EnhancedMessage = make([]EnhancedMessage, count) // There is an edge case that can happen on Android when the app is shutdown while fetching messages... // The worker threads that are spawned can become activated again when the app is opened attempt to finish their job... // In that case we skip processing and just return the empty message... // Note: This is far less likely to happen now that the UI only requests messages *after* syncing has happened and // these requests complete almost immediately v.s. being stalled for seconds to minutes on large groups. if application != nil { profile := application.GetPeer(profileOnion) if profile != nil { messages, err := profile.GetMostRecentMessages(conversationID, 0, messageIndex, count) if err == nil { for i, message := range messages { time, _ := time.Parse(time.RFC3339Nano, message.Attr[constants2.AttrSentTimestamp]) emessages[i].Message = model.Message{ Message: message.Body, Acknowledged: message.Attr[constants2.AttrAck] == constants2.True, Error: message.Attr[constants2.AttrErr], PeerID: message.Attr[constants2.AttrAuthor], Timestamp: time, } emessages[i].ID = message.ID emessages[i].Attributes = message.Attr emessages[i].ContactImage = utils.RandomProfileImage(emessages[i].PeerID) emessages[i].ContentHash = model.CalculateContentHash(message.Attr[constants2.AttrAuthor], message.Body) } } } } bytes, _ := json.Marshal(emessages) return string(bytes) } //export c_FreePointer // Dangerous function. Should only be used as documented in `MEMORY.md` func c_FreePointer(ptr *C.char) { C.free(unsafe.Pointer(ptr)) } //export c_SendMessage func c_SendMessage(profile_ptr *C.char, profile_len C.int, conversation_id C.int, msg_ptr *C.char, msg_len C.int) *C.char { profile := C.GoStringN(profile_ptr, profile_len) msg := C.GoStringN(msg_ptr, msg_len) return C.CString(SendMessage(profile, int(conversation_id), msg)) } func SendMessage(profileOnion string, conversationID int, msg string) string { profile := application.GetPeer(profileOnion) if profile != nil { id, err := profile.SendMessage(conversationID, msg) if err == nil { return GetMessageByID(profileOnion, conversationID, id) } } return "" } //export c_SendInvitation func c_SendInvitation(profile_ptr *C.char, profile_len C.int, conversation_id C.int, target_id C.int) *C.char { profile := C.GoStringN(profile_ptr, profile_len) return C.CString(SendInvitation(profile, int(conversation_id), int(target_id))) } // SendInvitation sends an invitation from `profileOnion` to contact `handle` (peer or group) // asking them to add the contact `target` (also peer or group). // For groups, the profile must already have `target` as a contact. func SendInvitation(profileOnion string, conversationID int, targetID int) string { profile := application.GetPeer(profileOnion) if profile != nil { id, err := profile.SendInviteToConversation(conversationID, targetID) if err == nil { return GetMessageByID(profileOnion, conversationID, id) } } return "" } //export c_ShareFile func c_ShareFile(profile_ptr *C.char, profile_len C.int, conversation_id C.int, filepath_ptr *C.char, filepath_len C.int) *C.char { profile := C.GoStringN(profile_ptr, profile_len) sharefilepath := C.GoStringN(filepath_ptr, filepath_len) return C.CString(ShareFile(profile, int(conversation_id), sharefilepath)) } func ShareFile(profileOnion string, conversationID int, sharefilepath string) string { profile := application.GetPeer(profileOnion) if profile != nil { fh, err := filesharing.FunctionalityGate(utils.ReadGlobalSettings().Experiments) if err != nil { log.Errorf("file sharing error: %v", err) } else { fileKey, overlay, err := fh.ShareFile(sharefilepath, profile) if err != nil { log.Errorf("error sharing file: %v", err) } else if conversationID == -1 { // FIXME: At some point we might want to allow arbitrary public files, but for now this API will assume // there is only one, and it is the custom profile image... profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants2.CustomProfileImageKey, fileKey) } else { id, err := profile.SendMessage(conversationID, overlay) if err == nil { return GetMessageByID(profileOnion, conversationID, id) } } } } return "" } //export c_DownloadFile func c_DownloadFile(profile_ptr *C.char, profile_len C.int, conversation_id C.int, filepath_ptr *C.char, filepath_len C.int, manifestpath_ptr *C.char, manifestpath_len C.int, filekey_ptr *C.char, filekey_len C.int) { profile := C.GoStringN(profile_ptr, profile_len) downloadfilepath := C.GoStringN(filepath_ptr, filepath_len) manifestpath := C.GoStringN(manifestpath_ptr, manifestpath_len) filekey := C.GoStringN(filekey_ptr, filekey_len) DownloadFile(profile, int(conversation_id), downloadfilepath, manifestpath, filekey) } func DownloadFile(profileOnion string, conversationID int, filepath, manifestpath, filekey string) { profile := application.GetPeer(profileOnion) if profile != nil { fh, err := filesharing.FunctionalityGate(utils.ReadGlobalSettings().Experiments) if err != nil { log.Errorf("file sharing error: %v", err) } else { // default to max 10 GB limit... fh.DownloadFile(profile, conversationID, filepath, manifestpath, filekey, files.MaxManifestSize*files.DefaultChunkSize) } } } //export c_CheckDownloadStatus func c_CheckDownloadStatus(profilePtr *C.char, profileLen C.int, fileKeyPtr *C.char, fileKeyLen C.int) { CheckDownloadStatus(C.GoStringN(profilePtr, profileLen), C.GoStringN(fileKeyPtr, fileKeyLen)) } func CheckDownloadStatus(profileOnion, fileKey string) { profile := application.GetPeer(profileOnion) if profile != nil { path, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)) if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True { eventHandler.Push(event.NewEvent(event.FileDownloaded, map[event.Field]string{ ProfileOnion: profileOnion, event.FileKey: fileKey, event.FilePath: path, event.TempFile: "", })) } else { log.Infof("CheckDownloadStatus found .path but not .complete") eventHandler.Push(event.NewEvent(event.FileDownloadProgressUpdate, map[event.Field]string{ ProfileOnion: profileOnion, event.FileKey: fileKey, event.Progress: "-1", event.FileSizeInChunks: "-1", event.FilePath: path, })) } } } //export c_VerifyOrResumeDownload func c_VerifyOrResumeDownload(profile_ptr *C.char, profile_len C.int, conversation_id C.int, filekey_ptr *C.char, filekey_len C.int) { profile := C.GoStringN(profile_ptr, profile_len) filekey := C.GoStringN(filekey_ptr, filekey_len) VerifyOrResumeDownload(profile, int(conversation_id), filekey) } func VerifyOrResumeDownload(profileOnion string, conversationID int, fileKey string) { profile := application.GetPeer(profileOnion) if profile != nil { if manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey)); exists { if downloadfilepath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)); exists { log.Infof("resuming %s", fileKey) DownloadFile(profileOnion, conversationID, downloadfilepath, manifestFilePath, fileKey) } else { log.Errorf("found manifest path but not download path for %s", fileKey) } } else { log.Errorf("no stored manifest path found for %s", fileKey) } } } //export c_ResetTor func c_ResetTor() { ResetTor() } func ResetTor() { log.Infof("Replacing ACN with new Tor...") settings := utils.ReadGlobalSettings() globalACN.Close() // we need to close first if dateDir is the same, otherwise buildACN can't launch tor. globalACN.ReplaceACN(buildACN(*settings, globalTorPath, globalAppDir)) // We need to update settings on reset as buildACN can alter settings, otherwise the next reset will be broken... settings = utils.ReadGlobalSettings() settingsJson, _ := json.Marshal(settings) application.GetPrimaryBus().Publish(event.NewEvent(utils.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) log.Infof("Restarted") } //export c_CreateGroup func c_CreateGroup(profile_ptr *C.char, profile_len C.int, server_ptr *C.char, server_len C.int, name_ptr *C.char, name_len C.int) { profile := C.GoStringN(profile_ptr, profile_len) server := C.GoStringN(server_ptr, server_len) name := C.GoStringN(name_ptr, name_len) CreateGroup(profile, server, name) } // CreateGroup takes in a profile and server in addition to a name and creates a new group. func CreateGroup(profileHandle string, server string, name string) { profile := application.GetPeer(profileHandle) if profile != nil { _, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { conversationID, err := profile.StartGroup(name, server) if err == nil { log.Debugf("created group %v on %v: $v", profileHandle, server, conversationID) } else { log.Errorf("error creating group or %v on server %v: %v", profileHandle, server, err) } } } } //export c_DeleteProfile func c_DeleteProfile(profile_ptr *C.char, profile_len C.int, password_ptr *C.char, password_len C.int) { profile := C.GoStringN(profile_ptr, profile_len) password := C.GoStringN(password_ptr, password_len) DeleteProfile(profile, password) } // DeleteProfile deletes a profile given the right password func DeleteProfile(profile string, password string) { // allow a blank password to delete "unencrypted" accounts... if password == "" { password = constants.DefactoPasswordForUnencryptedProfiles } application.DeletePeer(profile, password) } //export c_ArchiveConversation func c_ArchiveConversation(profile_ptr *C.char, profile_len C.int, conversation_id C.int) { profile := C.GoStringN(profile_ptr, profile_len) ArchiveConversation(profile, int(conversation_id)) } // ArchiveConversation sets the conversation to archived func ArchiveConversation(profileHandle string, conversationID int) { profile := application.GetPeer(profileHandle) if profile != nil { profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Archived)), constants2.True) } } //export c_DeleteContact func c_DeleteContact(profile_ptr *C.char, profile_len C.int, conversation_id C.int) { profile := C.GoStringN(profile_ptr, profile_len) DeleteContact(profile, int(conversation_id)) } // DeleteContact removes all trace of the contact from the profile func DeleteContact(profileHandle string, conversationID int) { profile := application.GetPeer(profileHandle) if profile != nil { profile.DeleteConversation(conversationID) } } //export c_ImportBundle func c_ImportBundle(profile_ptr *C.char, profile_len C.int, bundle_ptr *C.char, bundle_len C.int) { profile := C.GoStringN(profile_ptr, profile_len) name := C.GoStringN(bundle_ptr, bundle_len) ImportBundle(profile, name) } // ImportBundle takes in a handle to a profile and an invite string which could have one of many // different formats (e.g. a peer address, a group invite, a server key bundle, or a combination) func ImportBundle(profileOnion string, bundle string) { profile := application.GetPeer(profileOnion) if profile != nil { response := profile.ImportBundle(bundle) // We might have added a new server, so refresh the server list if applicable... groupHandler, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { serverListForOnion := groupHandler.GetServerInfoList(profile) serversListBytes, _ := json.Marshal(serverListForOnion) eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)})) } eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: response.Error()})) } } //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 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 { log.Errorf("attempted to set an attribute with an unknown zone: %v", key) } } } // Attribute is a struct to return the dual values of an attempt at a Get*Attribute API call, meant to be json searlialized type Attribute struct { Exists bool Value string } //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_SetMessageAttribute func c_SetMessageAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, channel_id C.int, message_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) SetMessageAttribute(profileOnion, int(conversation_id), int(channel_id), int(message_id), key, value) } // SetMessageAttribute is a wrapper around `UpdateMessageAttribute` on profile that allows the creation or update of a // given message attribute on a conversation/channel. // Errors if `profileOnion` is not associated to an existing & loaded profile, // of if `UpdateMessageAttribute` fails func SetMessageAttribute(profileOnion string, conversationID int, channelID int, messageID int, attributeKey string, attributeValue string) { profile := application.GetPeer(profileOnion) if profile == nil { log.Errorf("called SetMessageAttribute with invalid profile handle: %v", profileOnion) return } err := profile.UpdateMessageAttribute(conversationID, channelID, messageID, attributeKey, attributeValue) if err != nil { log.Errorf("error updating message attribute: %v", err) } } //export c_ChangePassword func c_ChangePassword(profile_ptr *C.char, profile_len C.int, oldpassword_ptr *C.char, oldpassword_len C.int, newpassword_ptr *C.char, newpassword_len C.int, newpassword_again_ptr *C.char, newpassword_again_len C.int) { profileOnion := C.GoStringN(profile_ptr, profile_len) oldPassword := C.GoStringN(oldpassword_ptr, oldpassword_len) newPassword := C.GoStringN(newpassword_ptr, newpassword_len) newPasswordAgain := C.GoStringN(newpassword_again_ptr, newpassword_again_len) ChangePassword(profileOnion, oldPassword, newPassword, newPasswordAgain) } // ChangePassword provides a wrapper around profile.ChangePassword func ChangePassword(profileOnion string, oldPassword string, newPassword string, newPasswordAgain string) { profile := application.GetPeer(profileOnion) if profile != nil { log.Infof("changing password for %v", profileOnion) err := profile.ChangePassword(oldPassword, newPassword, newPasswordAgain) log.Infof("change password result %v", err) if err == nil { eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: features.ConstructResponse("changepassword", constants.StatusSuccess).Error()})) } else { eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: features.ConstructResponse("changepassword", err.Error()).Error()})) } } } //export c_ExportProfile func c_ExportProfile(profile_ptr *C.char, profile_len C.int, file_ptr *C.char, file_len C.int) { profileOnion := C.GoStringN(profile_ptr, profile_len) file := C.GoStringN(file_ptr, file_len) ExportProfile(profileOnion, file) } // ExportProfile provides a wrapper around profile.ExportProfile func ExportProfile(profileOnion string, file string) { profile := application.GetPeer(profileOnion) if profile != nil { profile.Export(file) } } //export c_ImportProfile func c_ImportProfile(file_ptr *C.char, file_len C.int, passwordPtr *C.char, passwordLen C.int) *C.char { password := C.GoStringN(passwordPtr, passwordLen) exportedCwtchFile := C.GoStringN(file_ptr, file_len) return C.CString(ImportProfile(exportedCwtchFile, password)) } // ImportProfile is a wrapper around application.ImportProfile func ImportProfile(exportedCwtchFile string, pass string) string { _, err := application.ImportProfile(exportedCwtchFile, pass) if err == nil { return "" } return err.Error() } //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{})) servers.Shutdown() // 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 } } //***** Server APIs ***** //export c_LoadServers func c_LoadServers(passwordPtr *C.char, passwordLen C.int) { LoadServers(C.GoStringN(passwordPtr, passwordLen)) } func LoadServers(password string) { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { serversList, err := serversHandler.LoadServers(password) if err != nil { log.Errorf("Error attempting to load servers :%s\n", err) application.GetPrimaryBus().Publish(event.NewEventList(servers.ZeroServersLoaded)) } else if len(serversList) == 0 { application.GetPrimaryBus().Publish(event.NewEventList(servers.ZeroServersLoaded)) } else { for _, serverOnion := range serversList { serverInfo := serversHandler.GetServerInfo(serverOnion) log.Debugf("Load Server NewServer event: %s", serverInfo) ev := event.NewEvent(servers.NewServer, make(map[event.Field]string)) serverInfo.EnrichEvent(&ev) application.GetPrimaryBus().Publish(ev) if serverInfo.Autostart { LaunchServer(serverOnion) } } } } } //export c_CreateServer func c_CreateServer(passwordPtr *C.char, passwordLen C.int, descPtr *C.char, descLen C.int, autostart C.char) { CreateServer(C.GoStringN(passwordPtr, passwordLen), C.GoStringN(descPtr, descLen), autostart == 1) } func CreateServer(password string, description string, autostart bool) { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { s, err := serversHandler.CreateServer(password) if err != nil { log.Errorf("Could not create new server: %s\n", err) } else { s.SetAttribute(server.AttrDescription, description) if autostart { s.SetAttribute(server.AttrAutostart, "true") } else { s.SetAttribute(server.AttrAutostart, "false") } if password == constants.DefactoPasswordForUnencryptedProfiles { s.SetAttribute(server.AttrStorageType, server.StorageTypeDefaultPassword) } else { s.SetAttribute(server.AttrStorageType, server.StorageTypePassword) } serverInfo := serversHandler.GetServerInfo(s.Onion()) log.Debugf("Creating Server NewServer event: %s", serverInfo) ev := event.NewEvent(servers.NewServer, make(map[event.Field]string)) serverInfo.EnrichEvent(&ev) application.GetPrimaryBus().Publish(ev) if autostart { LaunchServer(s.Onion()) } } } } //export c_DeleteServer func c_DeleteServer(onionPtr *C.char, onionLen C.int, currentPasswordPtr *C.char, currentPasswordLen C.int) { DeleteServer(C.GoStringN(onionPtr, onionLen), C.GoStringN(currentPasswordPtr, currentPasswordLen)) } func DeleteServer(onion string, currentPassword string) { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { serversHandler.StopServer(onion) application.GetPrimaryBus().Publish(event.NewEventList(servers.ServerIntentUpdate, event.Identity, onion, servers.Intent, servers.IntentStopped)) err := serversHandler.DeleteServer(onion, currentPassword) if err == nil { application.GetPrimaryBus().Publish(event.NewEventList(servers.ServerDeleted, event.Status, constants.StatusSuccess, event.Identity, onion)) } else { application.GetPrimaryBus().Publish(event.NewEventList(servers.ServerDeleted, event.Status, constants.StatusError, event.Error, err.Error(), event.Identity, onion)) } } } //export c_LaunchServers func c_LaunchServers() { LaunchServers() } func LaunchServers() { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { for _, onion := range serversHandler.ListServers() { autostart := false if s := serversHandler.GetServer(onion); s != nil { autostart = s.GetAttribute(server.AttrAutostart) == "true" } if autostart { LaunchServer(onion) } } } } //export c_LaunchServer func c_LaunchServer(onionPtr *C.char, onionLen C.int) { LaunchServer(C.GoStringN(onionPtr, onionLen)) } func LaunchServer(onion string) { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { serversHandler.LaunchServer(onion) application.GetPrimaryBus().Publish(event.NewEventList(servers.ServerIntentUpdate, event.Identity, onion, servers.Intent, servers.IntentRunning)) } } //export c_StopServer func c_StopServer(onionPtr *C.char, onionLen C.int) { StopServer(C.GoStringN(onionPtr, onionLen)) } func StopServer(onion string) { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { serversHandler.StopServer(onion) application.GetPrimaryBus().Publish(event.NewEventList(servers.ServerIntentUpdate, event.Identity, onion, servers.Intent, servers.IntentStopped)) } } //export c_StopServers func c_StopServers() { StopServers() } func StopServers() { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { for _, onion := range serversHandler.ListServers() { StopServer(onion) } } } //export c_DestroyServers func c_DestroyServers() { DestroyServers() } func DestroyServers() { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { serversHandler.DestroyServers() } } //export c_SetServerAttribute func c_SetServerAttribute(onionPtr *C.char, onionLen C.int, keyPtr *C.char, keyLen C.int, valPtr *C.char, valLen C.int) { SetServerAttribute(C.GoStringN(onionPtr, onionLen), C.GoStringN(keyPtr, keyLen), C.GoStringN(valPtr, valLen)) } func SetServerAttribute(onion string, key string, val string) { serversHandler, err := servers.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { server := serversHandler.GetServer(onion) if server != nil { server.SetAttribute(key, val) } } } // ***** END Server APIs ***** //export c_GetDebugInfo func c_GetDebugInfo() *C.char { return C.CString(GetDebugInfo()) } type DebugInfo struct { 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.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) } // Leave as is, needed by ffi func main() {}