autobindings/templates/lib_template.go

620 lines
21 KiB
Go

//package cwtch
package main
// //Needed to invoke C.free
// #include <stdlib.h>
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() {}