package main import ( "crypto/rand" libapp "cwtch.im/cwtch/app" "cwtch.im/cwtch/event" "cwtch.im/cwtch/event/bridge" "cwtch.im/cwtch/peer" "cwtch.im/ui/go/features/servers" "cwtch.im/ui/go/handlers" os2 "cwtch.im/ui/go/os" "cwtch.im/ui/go/the" "cwtch.im/ui/go/ui" "encoding/base64" "flag" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" "github.com/therecipe/qt/androidextras" "github.com/therecipe/qt/core" "github.com/therecipe/qt/gui" "github.com/therecipe/qt/network" "github.com/therecipe/qt/qml" "github.com/therecipe/qt/quickcontrols2" mrand "math/rand" "os" "os/user" "path" "path/filepath" "runtime" "time" ) const androidBaseDir = "/data/data/ca.openprivacy.cwtch.ui/" var ( buildVer string buildDate string ) func init() { // make go-defined types available in qml ui.GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher") ui.MessageWrapper_QmlRegisterType2("CustomQmlTypes", 1, 0, "MessageWrapper") } func main() { if os.Getenv("CWTCH_FOLDER") != "" { the.CwtchDir = os.Getenv("CWTCH_FOLDER") } else if runtime.GOOS == "android" { the.CwtchDir = filepath.Join(androidBaseDir, "files") } else { usr, err := user.Current() if err != nil { log.Errorf("\nerror: could not load current user: %v\n", err) os.Exit(1) } the.CwtchDir = filepath.Join(usr.HomeDir, ".cwtch") } // suppress event.NewMessageFromPeer so we can handle it ourselves peer.DefaultEventsToHandle = []event.Type{ event.EncryptedGroupMessage, event.PeerAcknowledgement, event.NewGroupInvite, event.PeerError, event.SendMessageToGroupError, event.NewGetValMessageFromPeer, } logfileDefault := "cwtch_log.txt" flagDebug := flag.Bool("debug", false, "turn on extra debug level logging. WARNING: THIS MAY EXPOSE PRIVATE INFORMATION IN CONSOLE OUTPUT!") flagLogFile := flag.Bool("logfile", false, "instead of console output, log to $HOME/.cwtch/"+logfileDefault) flagLocal := flag.Bool("local", false, "load user interface from the local folder \"qml\" instead of the built-in UI") flagService := flag.Bool("service", false, "run this process as an android service") flagClientUI := flag.Bool("clientui", false, "start the UI as a client of a service app instead of a full app") flag.Parse() if *flagLogFile { filelogger, err := log.NewFile(log.LevelInfo, filepath.Join(the.CwtchDir, logfileDefault)) if err == nil { log.SetStd(filelogger) } } if *flagDebug { log.SetLevel(log.LevelDebug) } else { log.SetLevel(log.LevelInfo) } if !(*flagDebug && buildVer == "") { // PF Onions log.AddPrivacyFilter(func(s string) bool { return len(s) == 56 }) // PF Group ids log.AddPrivacyFilter(func(s string) bool { return len(s) == 32 }) // Replace in prod if buildVer != "" { log.SetPrivacyFilterReplace(true) } } log.ExcludeFromPattern("connection/connection") log.ExcludeFromPattern("event/eventmanager") log.ExcludeFromPattern("service.go") log.ExcludeFromPattern("tor/BaseOnionService.go") log.ExcludeFromPattern("applications/auth.go") log.ExcludeFromPattern("connections/engine.go") log.ExcludeFromPattern("bridge/pipeBridge.go") log.ExcludeFromPattern("app/appBridge.go") log.Infoln("ui main()") if buildVer == "" && os.Getenv("CWTCH_FOLDER") == "" { log.Infoln("Development build: using dev directory for dev profiles") the.CwtchDir = filepath.Join(the.CwtchDir, "dev") } the.ACN = nil the.Peer = nil the.IPCBridge = nil the.CwtchApp = nil the.CwtchService = nil os.MkdirAll(the.CwtchDir, 0700) os.MkdirAll(path.Join(the.CwtchDir, "tor"), 0700) if *flagService { mainService() } else { clientUI := *flagClientUI || runtime.GOOS == "android" mainUi(*flagLocal, clientUI) } if the.ACN != nil { the.ACN.Close() } } // QRunnable is a shim for QAndroidService and QGuiApplication type QRunnable interface { Exec() int } func mainService() { log.Infoln("I am the service") log.Infoln("Starting a cwtch app...") go loadNetworkingAndFiles(nil, true, false) var app QRunnable if runtime.GOOS == "android" { log.Infoln("Making QAndroidService...") app = androidextras.NewQAndroidService(len(os.Args), os.Args) } else { log.Infoln("Making QGuiApplication...") app = gui.NewQGuiApplication(len(os.Args), os.Args) } log.Infoln("Cwtch Service starting app.Exec") app.Exec() } func mainUi(flagLocal bool, flagClientUI bool) { log.Infof("I am the UI (client:%v)\n", flagClientUI) app := gui.NewQGuiApplication(len(os.Args), os.Args) // our globals err := ui.InitGlobalSettingsFile(filepath.Join(the.CwtchDir, "global"), the.AppPassword) if err != nil { log.Errorf("Could not access global ui config: %v\n", err) os.Exit(-1) } gcd := ui.NewGrandCentralDispatcher(nil) gcd.SetOs(runtime.GOOS) dir := core.QCoreApplication_ApplicationDirPath() log.Infof("core.QCoreApplication_ApplicationDirPath(): %v\n", dir) if runtime.GOOS == "android" { gcd.SetAssetPath("assets:/") } else if runtime.GOOS == "windows" { // all of these access are QML based, and QML takes URIs which use forward slashes and translates them to local OS sperators // also windows paths need to be like /c:/PATH dir = "/" + dir // QML uses '/' regardless of platform (so we use path.Join here not filepath.Join) gcd.SetAssetPath("file://" + path.Join(dir, "assets") + "/") } else { if buildVer == "" || flagLocal { if _, err := os.Stat(path.Join(dir, "assets")); !os.IsNotExist(err) { gcd.SetAssetPath("file://" + path.Join(dir, "assets") + "/") } } else { usr, err := user.Current() if err != nil { log.Errorf("\nerror: could not load current user: %v\n", err) os.Exit(1) } localCwtch := path.Join(usr.HomeDir, ".local/share/cwtch") if _, err := os.Stat(localCwtch); !os.IsNotExist(err) { gcd.SetAssetPath("file://" + path.Join(localCwtch, "assets") + "/") } else if _, err := os.Stat("/usr/share/cwtch"); !os.IsNotExist(err) { gcd.SetAssetPath("file://" + "/usr/share/cwtch/assets/") } else if _, err := os.Stat("/usr/local/share/cwtch/"); !os.IsNotExist(err) { gcd.SetAssetPath("file://" + "/usr/local/share/cwtch/assets/") } else if _, err := os.Stat(path.Join(dir, "assets")); !os.IsNotExist(err) { gcd.SetAssetPath("file://" + path.Join(dir, "assets") + "/") } } if gcd.AssetPath() == "" { log.Errorf("Could not find assets folder") os.Exit(-1) } } log.Infof("gcd.assetPath = '%v'\n", gcd.AssetPath()) if buildVer != "" { gcd.SetVersion(buildVer) gcd.SetBuildDate(buildDate) } else { gcd.SetVersion("development") gcd.SetBuildDate("now") } // this is to load local qml files quickly when developing var qmlSource *core.QUrl if flagLocal { qmlSource = core.QUrl_FromLocalFile("./qml/main.qml") } else { qmlSource = core.NewQUrl3("qrc:/qml/main.qml", 0) } app.SetWindowIcon(gui.NewQIcon5(":/qml/images/cwtch-icon.png")) // load english first so it becomes the default in case we don't have a .ts for the user's locale, or if it contains unfinished strings translator := core.NewQTranslator(nil) translator.Load("translation_en", ":/i18n/", "", "") core.QCoreApplication_InstallTranslator(translator) opaqueTranslator := core.NewQTranslator(nil) opaqueTranslator.Load("translation_en", ":/qml/opaque/i18n/", "", "") core.QCoreApplication_InstallTranslator(opaqueTranslator) core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, true) quickcontrols2.QQuickStyle_SetStyle("Universe") engine := qml.NewQQmlApplicationEngine(nil) gcd.QMLEngine = engine gcd.SetLocale(gcd.GlobalSettings.Locale) // prevent qt from initiating network connections (possible deanon attempts!) factory := qml.NewQQmlNetworkAccessManagerFactory() factory.ConnectCreate(func(parent *core.QObject) *network.QNetworkAccessManager { nam := network.NewQNetworkAccessManager(parent) nam.SetNetworkAccessible(network.QNetworkAccessManager__NotAccessible) proxy := network.NewQNetworkProxy() proxy.SetHostName("0.0.0.0") nam.SetProxy(proxy) return nam }) engine.SetNetworkAccessManagerFactory(factory) engine.RootContext().SetContextProperty("gcd", gcd) gcd.TimelineInterface = ui.NewMessageModel(nil) engine.RootContext().SetContextProperty("mm", gcd.TimelineInterface) engine.RootContext().SetContextProperty("androidCwtchActivity", gcd.AndroidCwtchActivity) engine.Load(qmlSource) go loadNetworkingAndFiles(gcd, false, flagClientUI) log.Infoln("Cwtch App starting app.Exec") app.Exec() } func loadACN() { torpath := "tor" if runtime.GOOS == "android" { torpath = filepath.Join(androidBaseDir, "lib/libtor.so") } else if runtime.GOOS == "windows" { ex, err := os.Executable() if err != nil { ex = "" } exPath := filepath.Dir(ex) torpath = filepath.Join(exPath, "tor-0.4.4.6", "Tor", "tor.exe") } else { dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) if _, err := os.Stat(filepath.Join(dir, "tor")); os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(dir, "deploy", "linux", "tor")); os.IsNotExist(err) { log.Warnln("Cannot find bundled Tor") } else { torpath = filepath.Join(dir, "deploy", "linux", "tor") } } else { torpath = filepath.Join(dir, "tor") } } // generate a random socks and control port (not real random...these are port numbers...) mrand.Seed(int64(time.Now().Nanosecond())) port := mrand.Intn(1000) + 9600 controlPort := port + 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) } // generate torrc on the fly // TODO if we have been configured for it, use system tor (like orbot) - we need a way to config this in the UI first tor.NewTorrc().WithSocksPort(port).WithOnionTrafficOnly().WithControlPort(controlPort).WithHashedPassword(base64.StdEncoding.EncodeToString(key)).Build(filepath.Join(the.CwtchDir, "tor", "torrc")) the.ACN, err = tor.NewTorACNWithAuth(the.CwtchDir, torpath, controlPort, tor.HashedPasswordAuthenticator{base64.StdEncoding.EncodeToString(key)}) if err != nil { // TODO: turn into UI error: status panel? log.Errorf("Could not start Tor: %v", err) os.Exit(1) } } func loadNetworkingAndFiles(gcd *ui.GrandCentralDispatcher, service bool, clientUI bool) { if service || clientUI || runtime.GOOS == "android" { clientIn := filepath.Join(the.CwtchDir, "clientIn") serviceIn := filepath.Join(the.CwtchDir, "serviceIn") if service { loadACN() serviceBridge := bridge.NewPipeBridgeService(serviceIn, clientIn) log.Infoln("Creating New App Service") the.CwtchService = libapp.NewAppService(the.ACN, the.CwtchDir, serviceBridge) } else { clientBridge := bridge.NewPipeBridgeClient(clientIn, serviceIn) log.Infoln("Creating New App Client") the.CwtchApp = libapp.NewAppClient(the.CwtchDir, clientBridge) } } else { if gcd.GlobalSettings.PreviousPid != -1 { // Todo: Check if there is an older version of the ACN running... log.Debugf("checking to see if we shutdown the previous ACN (%v)", gcd.GlobalSettings.PreviousPid) os2.CheckProcessAndKill(uint64(gcd.GlobalSettings.PreviousPid), "tor") } loadACN() pid, err := the.ACN.GetPID() if err == nil { gcd.GlobalSettings.PreviousPid = int64(pid) ui.WriteGlobalSettings(gcd.GlobalSettings) } else { log.Errorf("error fetching pid from ACN %v", err) } log.Infoln("Creating New App") the.CwtchApp = libapp.NewApp(the.ACN, the.CwtchDir) } if !service { the.AppBus = the.CwtchApp.GetPrimaryBus() subscribed := make(chan bool) go handlers.App(gcd, subscribed, clientUI) go servers.LaunchServiceManager(gcd, the.ACN, path.Join(the.CwtchDir, "servers")) <-subscribed } }