| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 1 | package main |
| 2 | |
| 3 | import ( |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 4 | "bytes" |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 5 | "crypto/rand" |
| 6 | "crypto/rsa" |
| 7 | "crypto/x509" |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 8 | "encoding/json" |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 9 | "errors" |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 10 | "flag" |
| 11 | "fmt" |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 12 | "image" |
| 13 | "image/png" |
| giolekva | 1026d2d | 2021-12-19 19:09:15 +0400 | [diff] [blame] | 14 | "strings" |
| giolekva | 52da88a | 2021-12-17 18:08:25 +0400 | [diff] [blame] | 15 | "time" |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 16 | |
| 17 | "gioui.org/app" |
| giolekva | 96202c5 | 2021-12-18 12:45:34 +0400 | [diff] [blame] | 18 | "gioui.org/font/gofont" |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 19 | "gioui.org/io/system" |
| 20 | "gioui.org/layout" |
| 21 | "gioui.org/op" |
| 22 | "gioui.org/unit" |
| giolekva | 96202c5 | 2021-12-18 12:45:34 +0400 | [diff] [blame] | 23 | "gioui.org/widget/material" |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 24 | "github.com/skip2/go-qrcode" |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 25 | ) |
| 26 | |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 27 | var vpnApiAddr = flag.String("vpn-api-addr", "", "VPN API server address") |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 28 | |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 29 | var p *processor |
| 30 | |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 31 | type ScanQRFor int |
| 32 | |
| 33 | const ( |
| 34 | ScanQRForJoin ScanQRFor = 0 |
| 35 | ScanQRForApprove = 1 |
| 36 | ) |
| 37 | |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 38 | type processor struct { |
| 39 | vc VPNClient |
| 40 | app App |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 41 | st Storage |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 42 | ui *UI |
| 43 | |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 44 | scanQRFor ScanQRFor |
| 45 | inviteQrCh chan image.Image |
| 46 | qrScannedCh chan []byte |
| 47 | joinQrCh chan image.Image |
| giolekva | 52da88a | 2021-12-17 18:08:25 +0400 | [diff] [blame] | 48 | |
| 49 | onConnectCh chan interface{} |
| 50 | onDisconnectCh chan interface{} |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 51 | |
| 52 | onConfigCh chan struct{} |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 53 | } |
| 54 | |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 55 | func newProcessor() *processor { |
| giolekva | 96202c5 | 2021-12-18 12:45:34 +0400 | [diff] [blame] | 56 | th := material.NewTheme(gofont.Collection()) |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 57 | app := createApp() |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 58 | return &processor{ |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 59 | vc: NewDirectVPNClient(*vpnApiAddr), |
| 60 | app: app, |
| 61 | st: app.CreateStorage(), |
| 62 | ui: NewUI(th, app.Capabilities()), |
| 63 | inviteQrCh: make(chan image.Image, 1), |
| 64 | qrScannedCh: make(chan []byte, 1), |
| 65 | joinQrCh: make(chan image.Image, 1), |
| 66 | onConnectCh: make(chan interface{}, 1), |
| 67 | onDisconnectCh: make(chan interface{}, 1), |
| 68 | onConfigCh: make(chan struct{}, 1), |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 69 | } |
| 70 | } |
| 71 | |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 72 | func (p *processor) QRCodeScanned(code []byte) { |
| 73 | p.qrScannedCh <- code |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 74 | } |
| 75 | |
| giolekva | 52da88a | 2021-12-17 18:08:25 +0400 | [diff] [blame] | 76 | func (p *processor) ConnectRequested(service interface{}) { |
| 77 | go func() { |
| 78 | time.Sleep(1 * time.Second) |
| 79 | p.onConnectCh <- service |
| 80 | }() |
| 81 | } |
| 82 | |
| 83 | func (p *processor) DisconnectRequested(service interface{}) { |
| 84 | p.onDisconnectCh <- service |
| 85 | } |
| 86 | |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 87 | func (p *processor) generatePublicPrivateKey() error { |
| 88 | config, err := p.st.Get() |
| 89 | if err != nil { |
| 90 | return err |
| 91 | } |
| 92 | if config.Enc.PrivateKey != nil { |
| 93 | return nil |
| 94 | } |
| 95 | privKey, err := rsa.GenerateKey(rand.Reader, 2048) |
| 96 | if err != nil { |
| 97 | panic(err) |
| 98 | } |
| 99 | config.Enc.PrivateKey = x509.MarshalPKCS1PrivateKey(privKey) |
| 100 | config.Enc.PublicKey = x509.MarshalPKCS1PublicKey(&privKey.PublicKey) |
| 101 | config.Network.PublicKey, config.Network.PrivateKey, err = x25519Keypair() |
| 102 | if err != nil { |
| 103 | return err |
| 104 | } |
| 105 | return p.st.Store(config) |
| 106 | } |
| 107 | |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 108 | func (p *processor) run() error { |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 109 | if err := p.generatePublicPrivateKey(); err != nil { |
| 110 | return err |
| 111 | } |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 112 | w := app.NewWindow( |
| 113 | app.Size(unit.Px(1500), unit.Px(1500)), |
| 114 | app.Title("PCloud"), |
| 115 | ) |
| 116 | var ops op.Ops |
| 117 | for { |
| 118 | select { |
| 119 | case e := <-w.Events(): |
| 120 | switch e := e.(type) { |
| 121 | case app.ViewEvent: |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 122 | if err := p.app.OnView(e); err != nil { |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 123 | return err |
| 124 | } else { |
| 125 | w.Invalidate() |
| 126 | } |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 127 | if config, err := p.st.Get(); err != nil { |
| 128 | return err |
| 129 | } else { |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 130 | if config.Network.Config != nil { |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 131 | p.onConfigCh <- struct{}{} |
| 132 | } |
| 133 | } |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 134 | case *system.CommandEvent: |
| 135 | if e.Type == system.CommandBack { |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 136 | if p.ui.OnBack() { |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 137 | e.Cancel = true |
| 138 | w.Invalidate() |
| 139 | } |
| 140 | } |
| 141 | case system.FrameEvent: |
| 142 | gtx := layout.NewContext(&ops, e) |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 143 | events := p.ui.Layout(gtx) |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 144 | e.Frame(&ops) |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 145 | if err := p.processUIEvents(events); err != nil { |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 146 | return err |
| 147 | } |
| 148 | } |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 149 | case img := <-p.inviteQrCh: |
| 150 | p.ui.InviteQRGenerated(img) |
| 151 | w.Invalidate() |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 152 | case code := <-p.qrScannedCh: |
| 153 | switch p.scanQRFor { |
| 154 | case ScanQRForJoin: |
| 155 | if err := p.JoinAndGetNetworkConfig(code); err != nil { |
| 156 | return err |
| 157 | } |
| 158 | case ScanQRForApprove: |
| 159 | if err := p.ApproveOther(code); err != nil { |
| 160 | return err |
| 161 | } |
| 162 | default: |
| 163 | return errors.New("Must not reach!") |
| giolekva | 52da88a | 2021-12-17 18:08:25 +0400 | [diff] [blame] | 164 | } |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 165 | case img := <-p.joinQrCh: |
| 166 | p.ui.JoinQRGenerated(img) |
| 167 | w.Invalidate() |
| 168 | go func() { |
| 169 | cnt := 0 |
| 170 | for { |
| 171 | fmt.Println(cnt) |
| 172 | cnt++ |
| 173 | if cnt > 5 { |
| 174 | break |
| 175 | } |
| 176 | time.Sleep(time.Second) |
| 177 | config, err := p.st.Get() |
| 178 | if err != nil { |
| 179 | continue |
| 180 | } |
| 181 | privKey, err := x509.ParsePKCS1PrivateKey(config.Enc.PrivateKey) |
| 182 | if err != nil { |
| 183 | continue |
| 184 | } |
| 185 | hostname, err := p.app.GetHostname() |
| 186 | if err != nil { |
| 187 | continue |
| 188 | } |
| 189 | hostname = sanitizeHostname(hostname) |
| 190 | network, err := p.vc.Get("https://vpn.lekva.me", hostname, privKey, config.Network.PrivateKey) |
| 191 | if err != nil { |
| 192 | continue |
| 193 | } |
| 194 | config.ApiAddr = "https://vpn.lekva.me" |
| 195 | config.Network.Config = network |
| 196 | if err := p.st.Store(config); err != nil { |
| 197 | continue |
| 198 | } |
| 199 | p.onConfigCh <- struct{}{} |
| 200 | break |
| 201 | } |
| 202 | }() |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 203 | case <-p.onConfigCh: |
| 204 | if err := p.app.TriggerService(); err != nil { |
| 205 | return err |
| 206 | } |
| 207 | case s := <-p.onConnectCh: |
| 208 | if err := p.app.UpdateService(s); err != nil { |
| 209 | return err |
| 210 | } |
| 211 | if config, err := p.st.Get(); err != nil { |
| 212 | return err |
| 213 | } else { |
| 214 | if err := p.app.Connect(config); err != nil { |
| 215 | return err |
| 216 | } |
| 217 | } |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 218 | } |
| 219 | } |
| 220 | return nil |
| 221 | } |
| 222 | |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 223 | func (p *processor) processUIEvents(events []UIEvent) error { |
| 224 | for _, e := range events { |
| 225 | switch e.(type) { |
| 226 | case EventGetInviteQRCode: |
| 227 | go func() { |
| 228 | if img, err := p.generateInviteQRCode(); err == nil { |
| 229 | p.inviteQrCh <- img |
| 230 | } else { |
| 231 | // TODO(giolekva): do not panic |
| 232 | panic(err) |
| 233 | } |
| 234 | }() |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 235 | case EventGetJoinQRCode: |
| 236 | go func() { |
| 237 | if img, err := p.generateJoinQRCode(); err == nil { |
| 238 | p.joinQrCh <- img |
| 239 | } else { |
| 240 | // TODO(giolekva): do not panic |
| 241 | panic(err) |
| 242 | } |
| 243 | }() |
| 244 | case EventApproveOther: |
| 245 | p.scanQRFor = ScanQRForApprove |
| 246 | if err := p.app.LaunchBarcodeScanner(); err != nil { |
| 247 | return err |
| 248 | } |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 249 | case EventScanBarcode: |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 250 | p.scanQRFor = ScanQRForJoin |
| 251 | if err := p.app.LaunchBarcodeScanner(); err != nil { |
| 252 | return err |
| 253 | } |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 254 | default: |
| 255 | return fmt.Errorf("Unhandled event: %#v", e) |
| 256 | } |
| 257 | } |
| 258 | return nil |
| 259 | } |
| 260 | |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 261 | type inviteQrCodeData struct { |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 262 | VPNApiAddr string `json:"vpn_api_addr"` |
| 263 | Message []byte `json:"message"` |
| 264 | Signature []byte `json:"signature"` |
| 265 | } |
| 266 | |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 267 | type joinQrCodeData struct { |
| 268 | EncPublicKey []byte `json:"enc_public_key"` |
| 269 | Name string `json:"name"` |
| 270 | NetPublicKey []byte `json:"net_public_key"` |
| 271 | IPCidr string `json:"ip_cidr"` |
| 272 | } |
| 273 | |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 274 | func (p *processor) generateInviteQRCode() (image.Image, error) { |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 275 | config, err := p.st.Get() |
| 276 | if err != nil { |
| 277 | return nil, err |
| 278 | } |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 279 | message := []byte("Hello PCloud") |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 280 | signature, err := p.vc.Sign(config.ApiAddr, message) |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 281 | if err != nil { |
| 282 | return nil, err |
| 283 | } |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 284 | c := inviteQrCodeData{ |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 285 | config.ApiAddr, |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 286 | message, |
| 287 | signature, |
| 288 | } |
| 289 | var data bytes.Buffer |
| 290 | if err := json.NewEncoder(&data).Encode(c); err != nil { |
| 291 | return nil, err |
| 292 | } |
| 293 | qr, err := qrcode.Encode(data.String(), qrcode.Medium, 1024) |
| 294 | if err != nil { |
| 295 | return nil, err |
| 296 | } |
| 297 | img, err := png.Decode(bytes.NewReader(qr)) |
| 298 | if err != nil { |
| 299 | return nil, err |
| 300 | } |
| 301 | return img, nil |
| 302 | } |
| 303 | |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 304 | func (p *processor) generateJoinQRCode() (image.Image, error) { |
| 305 | config, err := p.st.Get() |
| 306 | if err != nil { |
| 307 | return nil, err |
| 308 | } |
| 309 | hostname, err := p.app.GetHostname() |
| 310 | if err != nil { |
| 311 | return nil, err |
| 312 | } |
| 313 | hostname = sanitizeHostname(hostname) |
| 314 | c := joinQrCodeData{ |
| 315 | EncPublicKey: config.Enc.PublicKey, |
| 316 | Name: hostname, |
| 317 | NetPublicKey: config.Network.PublicKey, |
| 318 | IPCidr: "111.0.0.14/24", |
| 319 | } |
| 320 | var data bytes.Buffer |
| 321 | if err := json.NewEncoder(&data).Encode(c); err != nil { |
| 322 | return nil, err |
| 323 | } |
| 324 | qr, err := qrcode.Encode(data.String(), qrcode.Medium, 1024) |
| 325 | if err != nil { |
| 326 | return nil, err |
| 327 | } |
| 328 | img, err := png.Decode(bytes.NewReader(qr)) |
| 329 | if err != nil { |
| 330 | return nil, err |
| 331 | } |
| 332 | return img, nil |
| 333 | } |
| 334 | |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 335 | func (p *processor) JoinAndGetNetworkConfig(code []byte) error { |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 336 | config, err := p.st.Get() |
| 337 | if err != nil { |
| 338 | return err |
| 339 | } |
| 340 | var invite inviteQrCodeData |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 341 | if err := json.NewDecoder(bytes.NewReader(code)).Decode(&invite); err != nil { |
| giolekva | 1026d2d | 2021-12-19 19:09:15 +0400 | [diff] [blame] | 342 | return err |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 343 | } |
| giolekva | 1026d2d | 2021-12-19 19:09:15 +0400 | [diff] [blame] | 344 | hostname, err := p.app.GetHostname() |
| 345 | if err != nil { |
| 346 | return err |
| 347 | } |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 348 | hostname = sanitizeHostname(hostname) |
| 349 | network, err := p.vc.Join(invite.VPNApiAddr, hostname, config.Network.PublicKey, config.Network.PrivateKey, invite.Message, invite.Signature) |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 350 | if err != nil { |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 351 | return err |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 352 | } |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 353 | config.ApiAddr = invite.VPNApiAddr |
| 354 | config.Network.Config = network |
| 355 | if err := p.st.Store(config); err != nil { |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 356 | return err |
| giolekva | 52da88a | 2021-12-17 18:08:25 +0400 | [diff] [blame] | 357 | } |
| giolekva | 8d6a0ca | 2021-12-19 17:42:25 +0400 | [diff] [blame] | 358 | p.onConfigCh <- struct{}{} |
| 359 | return nil |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 360 | } |
| 361 | |
| giolekva | 3f0dcda | 2021-12-22 23:32:49 +0400 | [diff] [blame^] | 362 | func (p *processor) ApproveOther(code []byte) error { |
| 363 | config, err := p.st.Get() |
| 364 | if err != nil { |
| 365 | return err |
| 366 | } |
| 367 | var approve joinQrCodeData |
| 368 | if err := json.NewDecoder(bytes.NewReader(code)).Decode(&approve); err != nil { |
| 369 | return err |
| 370 | } |
| 371 | return p.vc.Approve(config.ApiAddr, approve.Name, approve.IPCidr, approve.EncPublicKey, approve.NetPublicKey) |
| 372 | } |
| 373 | |
| 374 | func sanitizeHostname(hostname string) string { |
| 375 | return strings.ToLower( |
| 376 | strings.ReplaceAll( |
| 377 | strings.ReplaceAll(hostname, " ", "-"), |
| 378 | ".", "-")) |
| 379 | } |
| 380 | |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 381 | func main() { |
| 382 | flag.Parse() |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 383 | p = newProcessor() |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 384 | go func() { |
| giolekva | f58a769 | 2021-12-15 18:05:39 +0400 | [diff] [blame] | 385 | if err := p.run(); err != nil { |
| giolekva | 313ee2b | 2021-12-15 15:17:29 +0400 | [diff] [blame] | 386 | panic(err) |
| 387 | } |
| 388 | }() |
| 389 | app.Main() |
| 390 | } |