2023-02-21 20:31:49 +00:00
//package cwtch
package main
// //Needed to invoke C.free
// #include <stdlib.h>
import "C"
import (
"cwtch.im/cwtch/event"
2023-02-28 17:56:09 +00:00
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
2023-02-21 20:31:49 +00:00
"encoding/json"
"fmt"
"git.openprivacy.ca/cwtch.im/cwtch-autobindings/utils"
2023-02-28 17:56:09 +00:00
"git.openprivacy.ca/openprivacy/connectivity/tor"
2023-02-21 20:31:49 +00:00
"git.openprivacy.ca/openprivacy/log"
"os"
"os/user"
path "path/filepath"
"runtime"
"strings"
"time"
2023-02-28 17:56:09 +00:00
mrand "math/rand"
"crypto/rand"
"encoding/base64"
2023-02-21 20:31:49 +00:00
"unsafe"
_ "github.com/mutecomm/go-sqlcipher/v4"
"cwtch.im/cwtch/app"
"git.openprivacy.ca/openprivacy/connectivity"
{ { 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 ( )
2023-02-28 17:56:09 +00:00
2023-02-21 20:31:49 +00:00
// 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 )
app . 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
2023-03-01 17:48:34 +00:00
settingsFile := app . LoadAppSettings ( appDir )
2023-02-21 20:31:49 +00:00
newACN , settings := buildACN ( settingsFile . ReadGlobalSettings ( ) , globalTorPath , globalAppDir )
2023-02-27 21:32:53 +00:00
globalACN = connectivity . NewProxyACN ( newACN )
2023-02-21 20:31:49 +00:00
settingsFile . WriteGlobalSettings ( settings )
application = app . NewApp ( & globalACN , appDir , settingsFile )
// Subscribe to all App Events...
eventHandler . HandleApp ( application )
// Settings may have changed...
settings = settings
settingsJson , _ := json . Marshal ( settings )
// 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...
2023-02-27 21:32:53 +00:00
application . UpdateSettings ( application . ReadSettings ( ) )
2023-02-21 20:31:49 +00:00
application . GetPrimaryBus ( ) . Publish ( event . NewEvent ( app . UpdateGlobalSettings , map [ event . Field ] string { event . Data : string ( settingsJson ) } ) )
log . Infof ( "libcwtch-go application launched" )
application . GetPrimaryBus ( ) . Publish ( event . NewEvent ( app . CwtchStarted , map [ event . Field ] string { } ) )
application . QueryACNVersion ( )
application . LoadProfiles ( app . DefactoPasswordForUnencryptedProfiles )
2023-02-28 17:56:09 +00:00
2023-03-01 17:48:34 +00:00
{ { EXPERIMENT_REGISTER } }
2023-02-21 20:31:49 +00:00
}
// 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
}
// TODO: Need To Repopulate UI
settingsJson , _ := json . Marshal ( application . ReadSettings ( ) )
application . GetPrimaryBus ( ) . Publish ( event . NewEvent ( app . UpdateGlobalSettings , map [ event . Field ] string { event . Data : string ( settingsJson ) } ) )
application . GetPrimaryBus ( ) . Publish ( event . NewEvent ( app . CwtchStarted , map [ event . Field ] string { } ) )
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
}
}
2023-02-28 17:56:09 +00:00
// 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 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 . PeerAutostart {
profile . SetScopedZonedAttribute ( attr . LocalScope , attr . ProfileZone , constants . PeerAutostart , 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 ) )
}
2023-02-21 20:31:49 +00:00
//export c_ResetTor
func c_ResetTor ( ) {
ResetTor ( )
}
func ResetTor ( ) {
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 := buildACN ( settings , globalTorPath , globalAppDir )
application . UpdateSettings ( settings )
globalACN . ReplaceACN ( newAcn )
application . QueryACNVersion ( )
// 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 ) } ) )
log . Infof ( "Restarted" )
}
2023-02-28 17:56:09 +00:00
const (
CwtchStarted = event . Type ( "CwtchStarted" )
CwtchStartError = event . Type ( "CwtchStartError" )
UpdateGlobalSettings = event . Type ( "UpdateGlobalSettings" )
)
func buildACN ( settings app . GlobalSettings , torPath string , appDir string ) ( connectivity . ACN , app . GlobalSettings ) {
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 ( CwtchStartError , event . Error , fmt . Sprintf ( "Error connecting to Tor: %v" , err ) ) )
return & connectivity . ErrorACN { } , settings
}
if settings . AllowAdvancedTorConfig {
controlPort = settings . CustomControlPort
socksPort = settings . CustomSocksPort
}
torrc := tor . NewTorrc ( ) . WithSocksPort ( socksPort ) . WithOnionTrafficOnly ( ) . WithControlPort ( controlPort ) . WithHashedPassword ( base64 . StdEncoding . EncodeToString ( key ) )
// torrc.WithLog(path.Join(appDir, "tor", "tor.log"), tor.TorLogLevelNotice)
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
}
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 ) ) )
return & connectivity . ErrorACN { } , settings
}
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 = os . MkdirTemp ( torDir , "data-dir-" ) ; err != nil {
eventHandler . Push ( event . NewEventList ( CwtchStartError , event . Error , fmt . Sprintf ( "Error connecting to Tor: %v" , err ) ) )
return & connectivity . ErrorACN { } , settings
}
}
// Persist Current Data Dir as Tor Cache...
settings . 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 ) ) )
acn = & connectivity . ErrorACN { }
}
return acn , settings
}
2023-02-27 21:32:53 +00:00
//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 app . GlobalSettings
json . Unmarshal ( [ ] byte ( settingsJson ) , & newSettings )
application . UpdateSettings ( newSettings )
2023-03-01 20:09:26 +00:00
{ { EXPERIMENT_UPDATESETTINGS } }
2023-02-27 21:32:53 +00:00
}
2023-02-28 17:56:09 +00:00
2023-02-21 20:31:49 +00:00
{ { BINDINGS } }
// Leave as is, needed by ffi
func main ( ) { }