From 119a65b9fe918a6b18e9b0617d2927e12b7ab7c2 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sun, 23 Jul 2023 13:12:55 +0200 Subject: [PATCH] refactor: further breaking up of model code --- conn.go | 119 ++++++++++++++++++++++++ input.go | 35 +++++++ msg.go => message.go | 4 + model.go | 215 +++++++------------------------------------ profile.go | 35 +++++++ 5 files changed, 224 insertions(+), 184 deletions(-) create mode 100644 conn.go rename msg.go => message.go (90%) create mode 100644 profile.go diff --git a/conn.go b/conn.go new file mode 100644 index 0000000..aaa4060 --- /dev/null +++ b/conn.go @@ -0,0 +1,119 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + mrand "math/rand" + "os" + "path" + "path/filepath" + "time" + + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/protocol/connections" + "cwtch.im/cwtch/settings" + "git.openprivacy.ca/openprivacy/connectivity/tor" + tea "github.com/charmbracelet/bubbletea" +) + +const ( + offline = iota + connecting + connected +) + +func ensureConnected(m model) (bool, tea.Msg) { + switch m.connState { + case offline: + return false, offlineMsg{} + case connecting: + return false, stillConnectingMsg{} + default: + return true, nil + } +} + +func attemptShutdown(m *model) tea.Msg { + if isConnected, msg := ensureConnected(*m); !isConnected { + return msg + } + + m.app.Shutdown() + m.acn.Close() + close(m.statusBuffer) + + return shutdownMsg{} +} + +func connInit(m *model) tea.Msg { + mrand.Seed(int64(time.Now().Nanosecond())) + port := mrand.Intn(1000) + 9600 + controlPort := port + 1 + + key := make([]byte, 64) + _, err := rand.Read(key) + if err != nil { + return errMsg{Err: fmt.Errorf("unable to generate control port password: %s", err)} + } + + if err := os.MkdirAll(path.Join(m.userDir, "/.tor", "tor"), 0700); err != nil { + return errMsg{Err: fmt.Errorf("unable to create tor directory: %s", err)} + } + + if err := os.MkdirAll(path.Join(m.userDir, "profiles"), 0700); err != nil { + return errMsg{Err: fmt.Errorf("unable to create profiles directory: %s", err)} + } + + if err := tor.NewTorrc(). + WithSocksPort(port). + WithOnionTrafficOnly(). + WithControlPort(controlPort). + WithHashedPassword(base64.StdEncoding.EncodeToString(key)). + Build(filepath.Join(m.userDir, ".tor", "tor", "torrc")); err != nil { + return errMsg{Err: fmt.Errorf("unable to initialise torrc builder: %s", err)} + } + + m.statusBuffer <- "initialising Tor ACN..." + + acn, err := tor.NewTorACNWithAuth( + path.Join(m.userDir, "/.tor"), + "", + path.Join(m.userDir, "/.tor", "data"), + controlPort, + tor.HashedPasswordAuthenticator{ + Password: base64.StdEncoding.EncodeToString(key), + }, + ) + if err != nil { + return errMsg{Err: fmt.Errorf("unable to bootstrap tor: %s", err)} + } + + m.statusBuffer <- "waiting for Tor ACN to bootstrap..." + + if err := acn.WaitTillBootstrapped(); err != nil { + return errMsg{Err: fmt.Errorf("unable to initialise tor: %s", err)} + } + + settingsFile, err := settings.InitGlobalSettingsFile(m.userDir, "") + if err != nil { + return errMsg{Err: fmt.Errorf("unable to initialise settings: %s", err)} + } + gSettings := settingsFile.ReadGlobalSettings() + gSettings.ExperimentsEnabled = false + gSettings.DownloadPath = "./" + settingsFile.WriteGlobalSettings(gSettings) + + m.statusBuffer <- "creating new Cwtch application" + + app := app.NewApp(acn, m.userDir, settingsFile) + + app.InstallEngineHooks(connections.DefaultEngineHooks{}) + + m.statusBuffer <- "the Tor ACN is up" + + return appInitialisedMsg{ + app: app, + acn: acn, + } +} diff --git a/input.go b/input.go index 8617a45..ac1c2a1 100644 --- a/input.go +++ b/input.go @@ -56,6 +56,14 @@ func isClearCmd(cmds []string) bool { } func handleCommand(cmd, hiddenInput string) tea.Msg { + if len(cmd) == 0 { + return nil + } + + if string(cmd[0]) != "/" { + return nil + } + cmds := strings.Split(cmd, " ") switch { @@ -102,3 +110,30 @@ func handleCommand(cmd, hiddenInput string) tea.Msg { return cmdMsg{output: "unknown command"} } } + +func hidePasswordInput(m *model) { + val := m.input.Value() + + if len(val) == 0 { + return + } + + if string(val[0]) != "/" { + return + } + + cmds := strings.Split(val[1:], " ") + if !(cmds[0] == "profile" && len(cmds) >= 2) { + return + } + + if cmds[1] == "create" && len(cmds) > 3 || cmds[1] == "unlock" && len(cmds) > 2 { + lastChar := string(val[len(val)-1]) + m.hiddenInput += lastChar + if lastChar != " " { + newCmd := []rune(val) + newCmd[len(val)-1] = '*' + m.input.SetValue(string(newCmd)) + } + } +} diff --git a/msg.go b/message.go similarity index 90% rename from msg.go rename to message.go index fc26c0b..587daf1 100644 --- a/msg.go +++ b/message.go @@ -22,6 +22,10 @@ type cmdMsg struct { output string } +type offlineMsg struct{} + +type stillConnectingMsg struct{} + type shutdownMsg struct{} type createProfileMsg struct { diff --git a/model.go b/model.go index 8b02bb9..0c34536 100644 --- a/model.go +++ b/model.go @@ -1,40 +1,18 @@ package main import ( - "crypto/rand" - "encoding/base64" "fmt" - mrand "math/rand" - "os" "path" - "path/filepath" "strings" - "time" "cwtch.im/cwtch/app" - "cwtch.im/cwtch/model/attr" - "cwtch.im/cwtch/model/constants" - "cwtch.im/cwtch/protocol/connections" - "cwtch.im/cwtch/settings" "git.openprivacy.ca/openprivacy/connectivity" - "git.openprivacy.ca/openprivacy/connectivity/tor" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) -const ( - offline = iota - connecting - connected -) - -type profile struct { - name string - onion string -} - type model struct { username string userDir string @@ -73,78 +51,6 @@ func newModel(username, homeDir string) model { } } -func (m model) initApp() tea.Msg { - mrand.Seed(int64(time.Now().Nanosecond())) - port := mrand.Intn(1000) + 9600 - controlPort := port + 1 - - key := make([]byte, 64) - _, err := rand.Read(key) - if err != nil { - return errMsg{Err: fmt.Errorf("unable to generate control port password: %s", err)} - } - - if err := os.MkdirAll(path.Join(m.userDir, "/.tor", "tor"), 0700); err != nil { - return errMsg{Err: fmt.Errorf("unable to create tor directory: %s", err)} - } - - if err := os.MkdirAll(path.Join(m.userDir, "profiles"), 0700); err != nil { - return errMsg{Err: fmt.Errorf("unable to create profiles directory: %s", err)} - } - - if err := tor.NewTorrc(). - WithSocksPort(port). - WithOnionTrafficOnly(). - WithControlPort(controlPort). - WithHashedPassword(base64.StdEncoding.EncodeToString(key)). - Build(filepath.Join(m.userDir, ".tor", "tor", "torrc")); err != nil { - return errMsg{Err: fmt.Errorf("unable to initialise torrc builder: %s", err)} - } - - m.statusBuffer <- "initialising Tor ACN..." - - acn, err := tor.NewTorACNWithAuth( - path.Join(m.userDir, "/.tor"), - "", - path.Join(m.userDir, "/.tor", "data"), - controlPort, - tor.HashedPasswordAuthenticator{ - Password: base64.StdEncoding.EncodeToString(key), - }, - ) - if err != nil { - return errMsg{Err: fmt.Errorf("unable to bootstrap tor: %s", err)} - } - - m.statusBuffer <- "waiting for Tor ACN to bootstrap..." - - if err := acn.WaitTillBootstrapped(); err != nil { - return errMsg{Err: fmt.Errorf("unable to initialise tor: %s", err)} - } - - settingsFile, err := settings.InitGlobalSettingsFile(m.userDir, "") - if err != nil { - return errMsg{Err: fmt.Errorf("unable to initialise settings: %s", err)} - } - gSettings := settingsFile.ReadGlobalSettings() - gSettings.ExperimentsEnabled = false - gSettings.DownloadPath = "./" - settingsFile.WriteGlobalSettings(gSettings) - - m.statusBuffer <- "creating new Cwtch application" - - app := app.NewApp(acn, m.userDir, settingsFile) - - app.InstallEngineHooks(connections.DefaultEngineHooks{}) - - m.statusBuffer <- "the Tor ACN is up" - - return appInitialisedMsg{ - app: app, - acn: acn, - } -} - func (m model) handleStatusMessage() tea.Msg { return statusMsg{msg: <-m.statusBuffer} } @@ -156,38 +62,6 @@ func (m model) Init() tea.Cmd { ) } -func (m model) isConnecting() bool { - if m.connState == connecting { - return true - } - - return false -} - -func (m model) isOffline() bool { - if m.connState == offline { - return true - } - - return false -} - -func (m model) shutdown() bool { - if m.isConnecting() { - return false - } - - if m.isOffline() { - return true - } - - m.app.Shutdown() - m.acn.Close() - close(m.statusBuffer) - - return true -} - func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmd tea.Cmd @@ -212,50 +86,35 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.String() { case "ctrl+c": - if m.shutdown() { - return m, tea.Quit - } + cmds = append(cmds, func() tea.Msg { + return attemptShutdown(&m) + }) case "enter": val := m.input.Value() - if len(val) == 0 { - break - } + cmds = append(cmds, func() tea.Msg { + return handleCommand(val[1:], m.hiddenInput) + }) - if string(val[0]) == "/" { - cmds = append(cmds, func() tea.Msg { - return handleCommand(val[1:], m.hiddenInput) - }) - } default: - val := m.input.Value() - if len(val) != 0 { - if string(val[0]) == "/" { - cmds := strings.Split(val[1:], " ") - if cmds[0] == "profile" && len(cmds) >= 2 { - if cmds[1] == "create" && len(cmds) > 3 || cmds[1] == "unlock" && len(cmds) > 2 { - lastChar := string(val[len(val)-1]) - m.hiddenInput += lastChar - if lastChar != " " { - newCmd := []rune(val) - newCmd[len(val)-1] = '*' - m.input.SetValue(string(newCmd)) - } - } - } - } - } + hidePasswordInput(&m) } case cmdMsg: m.input.Reset() m.statusBuffer <- msg.output + case offlineMsg: + m.input.Reset() + m.statusBuffer <- "conn: currently offline" + + case stillConnectingMsg: + m.input.Reset() + m.statusBuffer <- "conn: initialising, please hold" + case shutdownMsg: m.input.Reset() - if m.shutdown() { - return m, tea.Quit - } + return m, tea.Quit case createProfileMsg: m.hiddenInput = "" @@ -274,44 +133,30 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.hiddenInput = "" m.input.Reset() - switch m.connState { - case offline: - m.statusBuffer <- "offline, run /connect first?" - return m, tea.Batch(cmds...) - case connecting: - m.statusBuffer <- "still connecting, hold on a sec..." - return m, tea.Batch(cmds...) + if isConnected, msg := ensureConnected(m); !isConnected { + cmds = append(cmds, func() tea.Msg { return msg }) + break } - m.app.LoadProfiles(msg.password) - - var unlocked []profile - for _, onion := range m.app.ListProfiles() { - peer := m.app.GetPeer(onion) - name, _ := peer.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) - unlocked = append(unlocked, profile{name: name, onion: onion}) + profiles := unlockProfiles(m, msg.password) + if len(profiles) > 0 { + m.statusBuffer <- fmt.Sprintf("unlocked profile(s): %s", profiles.names()) } - - if len(unlocked) > 0 { - var names []string - for _, p := range unlocked { - names = append(names, p.name) - } - m.statusBuffer <- fmt.Sprintf("unlocked profile(s): %s", strings.Join(names, " ")) - } - - m.profiles = unlocked + m.profiles = profiles case profileInfoMsg: m.input.Reset() - if m.connState != connected { - m.statusBuffer <- "offline, run /connect first?" + + if isConnected, msg := ensureConnected(m); !isConnected { + cmds = append(cmds, func() tea.Msg { return msg }) break } + if len(m.profiles) == 0 { m.statusBuffer <- "no profiles loaded" break } + profile := m.profiles[m.profileState] m.statusBuffer <- fmt.Sprintf("profile onion: %s", profile.onion) @@ -328,7 +173,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case connectMsg: m.input.Reset() - cmds = append(cmds, m.initApp) + cmds = append(cmds, func() tea.Msg { + return connInit(&m) + }) case appInitialisedMsg: m.app = msg.app diff --git a/profile.go b/profile.go new file mode 100644 index 0000000..c9df972 --- /dev/null +++ b/profile.go @@ -0,0 +1,35 @@ +package main + +import ( + "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/model/constants" +) + +type profile struct { + name string + onion string +} + +type profiles []profile + +func (ps profiles) names() []string { + var names []string + for _, profile := range ps { + names = append(names, profile.name) + } + return names +} + +func unlockProfiles(m model, password string) profiles { + var unlocked []profile + + m.app.LoadProfiles(password) + + for _, onion := range m.app.ListProfiles() { + peer := m.app.GetPeer(onion) + name, _ := peer.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + unlocked = append(unlocked, profile{name: name, onion: onion}) + } + + return unlocked +}