-
Notifications
You must be signed in to change notification settings - Fork 1
/
katzen.go
347 lines (319 loc) · 8.26 KB
/
katzen.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package main
import (
"context"
_ "embed"
"flag"
"fmt"
"os"
"runtime"
"github.com/katzenpost/katzenpost/catshadow"
"github.com/katzenpost/katzenpost/client"
"time"
"gioui.org/app"
_ "gioui.org/app/permission/foreground"
_ "gioui.org/app/permission/storage"
"gioui.org/font/gofont"
"gioui.org/io/event"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget/material"
"gioui.org/x/notify"
"net/http"
_ "net/http/pprof"
)
const (
initialPKIConsensusTimeout = 45 * time.Second
notificationTimeout = 30 * time.Second
)
var (
dataDirName = "catshadow"
clientConfigFile = flag.String("f", "", "Path to the client config file.")
stateFile = flag.String("s", "catshadow_statefile", "Path to the client state file.")
debug = flag.Int("d", 0, "Enable golang debug service.")
th *material.Theme
minPasswordLen = 5 // XXX pick something reasonable
notifications = make(map[string]notify.Notification)
//go:embed default_config_without_tor.toml
cfgWithoutTor []byte
//go:embed default_config_with_tor.toml
cfgWithTor []byte
isConnected bool
isConnecting bool
)
type App struct {
endBg func()
w *app.Window
ops *op.Ops
c *catshadow.Client
stack pageStack
}
func newApp(w *app.Window) *App {
a := &App{
w: w,
ops: &op.Ops{},
}
return a
}
func (a *App) Layout(gtx layout.Context) {
a.update(gtx)
a.stack.Current().Layout(gtx)
}
func (a *App) update(gtx layout.Context) {
// handle global shortcuts
if backEvent(gtx) {
// XXX: this means that after signin, the top level page is homescreen
// and therefore pressing back won't logout
if a.stack.Len() > 1 {
a.stack.Pop()
return
}
}
if a.stack.Len() == 0 {
a.stack.Push(newSignInPage(a))
return
}
page := a.stack.Current()
if e := page.Event(gtx); e != nil {
switch e := e.(type) {
case RedrawEvent:
a.w.Invalidate()
case BackEvent:
a.stack.Pop()
case signInStarted:
p := newUnlockPage(e.result)
a.stack.Clear(p)
case unlockError:
isConnected = false
isConnecting = false
fmt.Printf("unlockError: %s\n", e.err)
a.stack.Clear(newSignInPage(a))
case restartClient:
isConnected = false
isConnecting = false
fmt.Printf("restartClient\n")
a.stack.Clear(newSignInPage(a))
case unlockSuccess:
// validate the statefile somehow
a.c = e.client
a.c.Start()
a.stack.Clear(newHomePage(a))
if _, err := a.c.GetBlob("AutoConnect"); err == nil {
go a.c.Online(context.TODO())
isConnecting = true
// if the client does not already have a spool
// descriptor, prompt to create one
spool := a.c.SpoolWriteDescriptor()
if spool == nil {
a.stack.Push(newSpoolPage(a))
}
}
case OfflineClick:
go a.c.Offline()
isConnected = false
isConnecting = false
case OnlineClick:
go a.c.Online(context.TODO())
isConnecting = true
spool := a.c.SpoolWriteDescriptor()
if spool == nil {
a.stack.Push(newSpoolPage(a))
}
case ShowSettingsClick:
a.stack.Push(newSettingsPage(a))
case AddContactClick:
a.stack.Push(newAddContactPage(a))
case AddContactComplete:
a.stack.Pop()
p := a.stack.Current()
// return to home page, and update the contacts page
if h, ok := p.(*HomePage); ok {
h.UpdateContacts()
}
case ChooseContactClick:
a.stack.Push(newConversationPage(a, e.nickname))
case ChooseAvatar:
a.stack.Push(newAvatarPicker(a, e.nickname, ""))
case ChooseAvatarPath:
a.stack.Pop()
a.stack.Push(newAvatarPicker(a, e.nickname, e.path))
case RenameContact:
a.stack.Push(newRenameContactPage(a, e.nickname))
case EditContact:
a.stack.Push(newEditContactPage(a, e.nickname))
case EditContactComplete:
a.stack.Clear(newHomePage(a))
case MessageSent:
}
}
}
func (a *App) run() error {
// on Android, this will start a foreground service, and does nothing on other platforms
cancelForeground, err := app.Start("Background Connection", "")
if err != nil {
return err
}
defer cancelForeground()
// only read window events until client is established
for {
if a.c != nil {
break
}
e := a.w.Event()
if err := a.handleGioEvents(e); err != nil {
return err
}
}
defer func() {
if a.c != nil {
a.c.Shutdown()
a.c.Wait()
}
}()
evCh := make(chan event.Event)
ackCh := make(chan struct{})
go func() {
for {
ev := a.w.Event()
evCh <- ev
<-ackCh
if _, ok := ev.(app.DestroyEvent); ok {
return
}
}
}()
// select from all event sources
for {
select {
case e := <-a.c.EventSink:
if err := a.handleCatshadowEvent(e); err != nil {
return err
}
case e := <-evCh:
if err := a.handleGioEvents(e); err != nil {
ackCh <- struct{}{}
return err
}
ackCh <- struct{}{}
case <-time.After(1 * time.Minute):
// redraw the screen to update the message timestamps once per minute
a.w.Invalidate()
}
}
}
func main() {
flag.Parse()
fmt.Println("Katzenpost is still pre-alpha. DO NOT DEPEND ON IT FOR STRONG SECURITY OR ANONYMITY.")
if *debug != 0 {
go func() {
http.ListenAndServe(fmt.Sprintf("localhost:%d", *debug), nil)
}()
runtime.SetMutexProfileFraction(1)
runtime.SetBlockProfileRate(1)
}
// Start graphical user interface.
uiMain()
}
func uiMain() {
go func() {
w := new(app.Window)
w.Option(app.Size(unit.Dp(400), unit.Dp(400)),
app.Title("Katzen"),
app.NavigationColor(rgb(0x0)),
app.StatusColor(rgb(0x0)),
)
// theme must be declared AFTER NewWindow on android
th = func() *material.Theme {
th := material.NewTheme()
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
th.Bg = rgb(0x0)
th.Fg = rgb(0xFFFFFFFF)
th.ContrastBg = rgb(0x22222222)
th.ContrastFg = rgb(0x77777777)
return th
}()
if err := newApp(w).run(); err != nil {
fmt.Fprintf(os.Stderr, "Failed: %v\n", err)
}
os.Exit(0)
}()
app.Main()
}
type (
C = layout.Context
D = layout.Dimensions
)
func (a *App) handleCatshadowEvent(e interface{}) error {
switch event := e.(type) {
case *client.ConnectionStatusEvent:
isConnecting = false
if event.IsConnected {
isConnected = true
go func() {
if n, err := notify.Push("Connected", "Katzen has connected"); err == nil {
<-time.After(notificationTimeout)
n.Cancel()
}
}()
} else {
isConnected = false
go func() {
if n, err := notify.Push("Disconnected", "Katzen has disconnected"); err == nil {
<-time.After(notificationTimeout)
n.Cancel()
}
}()
}
if event.Err != nil {
go func() {
if n, err := notify.Push("Error", fmt.Sprintf("Katzen error: %s", event.Err)); err == nil {
<-time.After(notificationTimeout)
n.Cancel()
}
}()
}
case *catshadow.KeyExchangeCompletedEvent:
if event.Err != nil {
if n, err := notify.Push("Key Exchange", fmt.Sprintf("Failed: %s", event.Err)); err == nil {
go func() { <-time.After(notificationTimeout); n.Cancel() }()
}
} else {
if n, err := notify.Push("Key Exchange", fmt.Sprintf("Completed: %s", event.Nickname)); err == nil {
go func() { <-time.After(notificationTimeout); n.Cancel() }()
}
}
case *catshadow.MessageNotSentEvent:
if n, err := notify.Push("Message Not Sent", fmt.Sprintf("Failed to send message to %s", event.Nickname)); err == nil {
go func() { <-time.After(notificationTimeout); n.Cancel() }()
}
case *catshadow.MessageReceivedEvent:
// do not notify for the focused conversation
p := a.stack.Current()
switch p := p.(type) {
case *conversationPage:
// XXX: on android, input focus is not lost when the application does not have foreground
// but system.Stage is changed. On desktop linux, the stage does not change, but window focus is lost.
if p.nickname == event.Nickname {
a.w.Invalidate()
return nil
}
}
// emit a notification in all other cases
if n, err := notify.Push("Message Received", fmt.Sprintf("Message Received from %s", event.Nickname)); err == nil {
if o, ok := notifications[event.Nickname]; ok {
// cancel old notification before replacing with a new one
o.Cancel()
}
notifications[event.Nickname] = n
}
case *catshadow.MessageSentEvent:
case *catshadow.MessageDeliveredEvent:
default:
// do not invalidate window for events we do not care about
return nil
}
// redraw the screen when an event we care about is received
a.w.Invalidate()
return nil
}