blob: 38c55ffe55dc0388a843d46b047a11e6d03bdd89 [file] [log] [blame]
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"errors"
"flag"
"fmt"
"image"
"image/png"
"strings"
"time"
"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
"gioui.org/widget/material"
"github.com/skip2/go-qrcode"
)
var vpnApiAddr = flag.String("vpn-api-addr", "", "VPN API server address")
var p *processor
type ScanQRFor int
const (
ScanQRForJoin ScanQRFor = 0
ScanQRForApprove = 1
)
type processor struct {
vc VPNClient
app App
st Storage
ui *UI
scanQRFor ScanQRFor
inviteQrCh chan image.Image
qrScannedCh chan []byte
joinQrCh chan image.Image
onConnectCh chan interface{}
onDisconnectCh chan interface{}
onConfigCh chan struct{}
}
func newProcessor() *processor {
th := material.NewTheme(gofont.Collection())
app := createApp()
return &processor{
vc: NewDirectVPNClient(*vpnApiAddr),
app: app,
st: app.CreateStorage(),
ui: NewUI(th, app.Capabilities()),
inviteQrCh: make(chan image.Image, 1),
qrScannedCh: make(chan []byte, 1),
joinQrCh: make(chan image.Image, 1),
onConnectCh: make(chan interface{}, 1),
onDisconnectCh: make(chan interface{}, 1),
onConfigCh: make(chan struct{}, 1),
}
}
func (p *processor) QRCodeScanned(code []byte) {
p.qrScannedCh <- code
}
func (p *processor) ConnectRequested(service interface{}) {
go func() {
time.Sleep(1 * time.Second)
p.onConnectCh <- service
}()
}
func (p *processor) DisconnectRequested(service interface{}) {
p.onDisconnectCh <- service
}
func (p *processor) generatePublicPrivateKey() error {
config, err := p.st.Get()
if err != nil {
return err
}
if config.Enc.PrivateKey != nil {
return nil
}
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
config.Enc.PrivateKey = x509.MarshalPKCS1PrivateKey(privKey)
config.Enc.PublicKey = x509.MarshalPKCS1PublicKey(&privKey.PublicKey)
config.Network.PublicKey, config.Network.PrivateKey, err = x25519Keypair()
if err != nil {
return err
}
return p.st.Store(config)
}
func (p *processor) run() error {
if err := p.generatePublicPrivateKey(); err != nil {
return err
}
w := app.NewWindow(
app.Size(unit.Px(1500), unit.Px(1500)),
app.Title("PCloud"),
)
var ops op.Ops
for {
select {
case e := <-w.Events():
switch e := e.(type) {
case app.ViewEvent:
if err := p.app.OnView(e); err != nil {
return err
} else {
w.Invalidate()
}
if config, err := p.st.Get(); err != nil {
return err
} else {
if config.Network.Config != nil {
p.onConfigCh <- struct{}{}
}
}
case *system.CommandEvent:
if e.Type == system.CommandBack {
if p.ui.OnBack() {
e.Cancel = true
w.Invalidate()
}
}
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
events := p.ui.Layout(gtx)
e.Frame(&ops)
if err := p.processUIEvents(events); err != nil {
return err
}
}
case img := <-p.inviteQrCh:
p.ui.InviteQRGenerated(img)
w.Invalidate()
case code := <-p.qrScannedCh:
switch p.scanQRFor {
case ScanQRForJoin:
if err := p.JoinAndGetNetworkConfig(code); err != nil {
return err
}
case ScanQRForApprove:
if err := p.ApproveOther(code); err != nil {
return err
}
default:
return errors.New("Must not reach!")
}
case img := <-p.joinQrCh:
p.ui.JoinQRGenerated(img)
w.Invalidate()
go func() {
cnt := 0
for {
fmt.Println(cnt)
cnt++
if cnt > 5 {
break
}
time.Sleep(time.Second)
config, err := p.st.Get()
if err != nil {
continue
}
privKey, err := x509.ParsePKCS1PrivateKey(config.Enc.PrivateKey)
if err != nil {
continue
}
hostname, err := p.app.GetHostname()
if err != nil {
continue
}
hostname = sanitizeHostname(hostname)
network, err := p.vc.Get("https://vpn.lekva.me", hostname, privKey, config.Network.PrivateKey)
if err != nil {
continue
}
config.ApiAddr = "https://vpn.lekva.me"
config.Network.Config = network
if err := p.st.Store(config); err != nil {
continue
}
p.onConfigCh <- struct{}{}
break
}
}()
case <-p.onConfigCh:
if err := p.app.TriggerService(); err != nil {
return err
}
case s := <-p.onConnectCh:
if err := p.app.UpdateService(s); err != nil {
return err
}
if config, err := p.st.Get(); err != nil {
return err
} else {
if err := p.app.Connect(config); err != nil {
return err
}
}
}
}
return nil
}
func (p *processor) processUIEvents(events []UIEvent) error {
for _, e := range events {
switch e.(type) {
case EventGetInviteQRCode:
go func() {
if img, err := p.generateInviteQRCode(); err == nil {
p.inviteQrCh <- img
} else {
// TODO(giolekva): do not panic
panic(err)
}
}()
case EventGetJoinQRCode:
go func() {
if img, err := p.generateJoinQRCode(); err == nil {
p.joinQrCh <- img
} else {
// TODO(giolekva): do not panic
panic(err)
}
}()
case EventApproveOther:
p.scanQRFor = ScanQRForApprove
if err := p.app.LaunchBarcodeScanner(); err != nil {
return err
}
case EventScanBarcode:
p.scanQRFor = ScanQRForJoin
if err := p.app.LaunchBarcodeScanner(); err != nil {
return err
}
default:
return fmt.Errorf("Unhandled event: %#v", e)
}
}
return nil
}
type inviteQrCodeData struct {
VPNApiAddr string `json:"vpn_api_addr"`
Message []byte `json:"message"`
Signature []byte `json:"signature"`
}
type joinQrCodeData struct {
EncPublicKey []byte `json:"enc_public_key"`
Name string `json:"name"`
NetPublicKey []byte `json:"net_public_key"`
IPCidr string `json:"ip_cidr"`
}
func (p *processor) generateInviteQRCode() (image.Image, error) {
config, err := p.st.Get()
if err != nil {
return nil, err
}
message := []byte("Hello PCloud")
signature, err := p.vc.Sign(config.ApiAddr, message)
if err != nil {
return nil, err
}
c := inviteQrCodeData{
config.ApiAddr,
message,
signature,
}
var data bytes.Buffer
if err := json.NewEncoder(&data).Encode(c); err != nil {
return nil, err
}
qr, err := qrcode.Encode(data.String(), qrcode.Medium, 1024)
if err != nil {
return nil, err
}
img, err := png.Decode(bytes.NewReader(qr))
if err != nil {
return nil, err
}
return img, nil
}
func (p *processor) generateJoinQRCode() (image.Image, error) {
config, err := p.st.Get()
if err != nil {
return nil, err
}
hostname, err := p.app.GetHostname()
if err != nil {
return nil, err
}
hostname = sanitizeHostname(hostname)
c := joinQrCodeData{
EncPublicKey: config.Enc.PublicKey,
Name: hostname,
NetPublicKey: config.Network.PublicKey,
IPCidr: "111.0.0.14/24",
}
var data bytes.Buffer
if err := json.NewEncoder(&data).Encode(c); err != nil {
return nil, err
}
qr, err := qrcode.Encode(data.String(), qrcode.Medium, 1024)
if err != nil {
return nil, err
}
img, err := png.Decode(bytes.NewReader(qr))
if err != nil {
return nil, err
}
return img, nil
}
func (p *processor) JoinAndGetNetworkConfig(code []byte) error {
config, err := p.st.Get()
if err != nil {
return err
}
var invite inviteQrCodeData
if err := json.NewDecoder(bytes.NewReader(code)).Decode(&invite); err != nil {
return err
}
hostname, err := p.app.GetHostname()
if err != nil {
return err
}
hostname = sanitizeHostname(hostname)
network, err := p.vc.Join(invite.VPNApiAddr, hostname, config.Network.PublicKey, config.Network.PrivateKey, invite.Message, invite.Signature)
if err != nil {
return err
}
config.ApiAddr = invite.VPNApiAddr
config.Network.Config = network
if err := p.st.Store(config); err != nil {
return err
}
p.onConfigCh <- struct{}{}
return nil
}
func (p *processor) ApproveOther(code []byte) error {
config, err := p.st.Get()
if err != nil {
return err
}
var approve joinQrCodeData
if err := json.NewDecoder(bytes.NewReader(code)).Decode(&approve); err != nil {
return err
}
return p.vc.Approve(config.ApiAddr, approve.Name, approve.IPCidr, approve.EncPublicKey, approve.NetPublicKey)
}
func sanitizeHostname(hostname string) string {
return strings.ToLower(
strings.ReplaceAll(
strings.ReplaceAll(hostname, " ", "-"),
".", "-"))
}
func main() {
flag.Parse()
p = newProcessor()
go func() {
if err := p.run(); err != nil {
panic(err)
}
}()
app.Main()
}