From 3b6e3d9b79f93991dfa460abfc9b45bb33f19383 Mon Sep 17 00:00:00 2001
From: "kali kaneko (leap communications)" <kali@leap.se>
Date: Thu, 4 Jun 2020 11:43:53 +0200
Subject: [PATCH] [feat] add go wrapper

Signed-off-by: kali kaneko (leap communications) <kali@leap.se>
---
 bitmask.pro    |  72 +++++++++++
 gui/backend.go | 342 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 414 insertions(+)
 create mode 100644 bitmask.pro
 create mode 100644 gui/backend.go

diff --git a/bitmask.pro b/bitmask.pro
new file mode 100644
index 00000000..1afe1623
--- /dev/null
+++ b/bitmask.pro
@@ -0,0 +1,72 @@
+CONFIG += qt staticlib
+windows:CONFIG += console
+unix:DEBUG:CONFIG += debug
+lessThan(QT_MAJOR_VERSION, 5): error("requires Qt 5")
+
+# trying to optimize size of the static binary.
+# probably more can be shaved off with some patience
+# You need to recompile your version of Qt to use the libraries you want. The
+# information comes from the build configuration of the Qt version that you are
+# using. Simply point Qts configure to the relevant libraries you wish to
+# override, build it, and use it to build your project. It will automatically
+# pull in the newer libraries that you overrode.
+# TODO: patch the $(PKG)_BUILD definition in mxe/src/qtbase.mk and shave some options there.
+# https://stackoverflow.com/questions/5587141/recommended-flags-for-a-minimalistic-qt-build
+# See also: https://qtlite.com/
+
+#QTPLUGIN.imageformats = -
+#QTPLUGIN.QTcpServerConnectionFactory =-
+#QTPLUGIN.QQmlDebugServerFactory =-
+#QTPLUGIN.QWindowsIntegrationPlugin =-
+#QTPLUGIN.QQmlDebuggerServiceFactory =-
+#QTPLUGIN.QQmlInspectorServiceFactory =-
+#QTPLUGIN.QLocalClientConnectionFactory =-
+#QTPLUGIN.QDebugMessageServiceFactory =-
+#QTPLUGIN.QQmlNativeDebugConnectorFactory =-
+#QTPLUGIN.QQmlNativeDebugServiceFactory =-
+#QTPLUGIN.QQmlPreviewServiceFactory =-
+#QTPLUGIN.QQmlProfilerServiceFactory =-
+#QTPLUGIN.QQuickProfilerAdapterFactory =-
+#QTPLUGIN.QQmlDebugServerFactory =-
+#QTPLUGIN.QTcpServerConnectionFactory =-
+#QTPLUGIN.QGenericEnginePlugin =-
+
+QT += qml quick
+
+TARGET=minivpn
+
+SOURCES += \
+    gui/main.cpp \
+    gui/qjsonmodel.cpp \
+    gui/handlers.cpp
+
+RESOURCES += gui/gui.qrc
+
+HEADERS += \
+    gui/handlers.h \
+    gui/qjsonmodel.h \
+    lib/libgoshim.h
+
+LIBS += -L./lib -lgoshim -lpthread
+
+DESTDIR = release
+OBJECTS_DIR = release/.obj
+MOC_DIR = release/.moc
+RCC_DIR = release/.rcc
+UI_DIR = release/.ui
+
+Release:DESTDIR = release
+Release:DESTDIR = release
+Release:OBJECTS_DIR = release/.obj
+Release:MOC_DIR = release/.moc
+Release:RCC_DIR = release/.rcc
+Release:UI_DIR = release/.ui
+
+Debug:DESTDIR = debug
+Debug:OBJECTS_DIR = debug/.obj
+Debug:MOC_DIR = debug/.moc
+Debug:RCC_DIR = debug/.rcc
+Debug:UI_DIR = debug/.ui
+
+DISTFILES += \
+    README.md
diff --git a/gui/backend.go b/gui/backend.go
new file mode 100644
index 00000000..9a952a77
--- /dev/null
+++ b/gui/backend.go
@@ -0,0 +1,342 @@
+package main
+
+/* a wrapper around bitmask that exposes status to a QtQml gui */
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"reflect"
+	"sync"
+	"unsafe"
+
+	"0xacab.org/leap/bitmask-vpn/pkg/bitmask"
+	"github.com/jmshal/go-locale"
+	"github.com/kalikaneko/bitmask-vpn/pkg/systray2"
+	"golang.org/x/text/message"
+)
+
+// typedef void (*cb)();
+// inline void _do_callback(cb f) {
+// 	f();
+// }
+import "C"
+
+/* callbacks into C-land */
+
+var mut sync.Mutex
+var stmut sync.Mutex
+var cbs = make(map[string](*[0]byte))
+var initOnce sync.Once
+
+// Events are just a enumeration of all the posible events that C functions can
+// be interested in subscribing to. You cannot subscribe to an event that is
+// not listed here.
+type Events struct {
+	OnStatusChanged string
+}
+
+const OnStatusChanged string = "OnStatusChanged"
+
+// subscribe registers a callback from C-land.
+// This callback needs to be passed as a void* C function pointer.
+func subscribe(event string, fp unsafe.Pointer) {
+	mut.Lock()
+	defer mut.Unlock()
+	e := &Events{}
+	v := reflect.Indirect(reflect.ValueOf(&e))
+	hf := v.Elem().FieldByName(event)
+	if reflect.ValueOf(hf).IsZero() {
+		fmt.Println("ERROR: not a valid event:", event)
+	} else {
+		cbs[event] = (*[0]byte)(fp)
+	}
+}
+
+// trigger fires a callback from C-land.
+func trigger(event string) {
+	mut.Lock()
+	defer mut.Unlock()
+	cb := cbs[event]
+	if cb != nil {
+		C._do_callback(cb)
+	} else {
+		fmt.Println("ERROR: this event does not have subscribers:", event)
+	}
+}
+
+/* connection status */
+
+const logFile = "systray.log"
+
+const (
+	offStr      = "off"
+	startingStr = "starting"
+	onStr       = "on"
+	stoppingStr = "stopping"
+	failedStr   = "failed"
+)
+
+// status reflects the current VPN status. Go code is responsible for updating
+// it; C-land just watches its changes and pulls its updates via the serialized
+// context object.
+type status int
+
+const (
+	off status = iota
+	starting
+	on
+	stopping
+	failed
+	unknown
+)
+
+func (s status) String() string {
+	return [...]string{offStr, startingStr, onStr, stoppingStr, failedStr}[s]
+}
+
+func (s status) MarshalJSON() ([]byte, error) {
+	b := bytes.NewBufferString(`"`)
+	b.WriteString(s.String())
+	b.WriteString(`"`)
+	return b.Bytes(), nil
+}
+
+func (s status) fromString(st string) status {
+	switch st {
+	case offStr:
+		return off
+	case startingStr:
+		return starting
+	case onStr:
+		return on
+	case stoppingStr:
+		return stopping
+	case failedStr:
+		return failed
+	default:
+		return unknown
+	}
+}
+
+// An action is originated in the UI. These represent requests coming from the
+// frontend via the C code. VPN code needs to watch them and fullfill their
+// requests as soon as possible.
+type actions int
+
+const (
+	switchOn actions = iota
+	switchOff
+	unblock
+)
+
+func (a actions) String() string {
+	return [...]string{"switchOn", "switchOff", "unblock"}[a]
+}
+
+func (a actions) MarshalJSON() ([]byte, error) {
+	b := bytes.NewBufferString(`"`)
+	b.WriteString(a.String())
+	b.WriteString(`"`)
+	return b.Bytes(), nil
+}
+
+// The connectionCtx keeps the global state that is passed around to C-land. It
+// also serves as the primary way of passing requests from the frontend to the
+// Go-core, by letting the UI write some of these variables and processing
+// them.
+type connectionCtx struct {
+	AppName  string    `json:"appName"`
+	Provider string    `json:"provider"`
+	Status   status    `json:"status"`
+	Actions  []actions `json:"actions,omitempty"`
+	bm       bitmask.Bitmask
+}
+
+func (c connectionCtx) toJson() ([]byte, error) {
+	stmut.Lock()
+	defer stmut.Unlock()
+	b, err := json.Marshal(c)
+	if err != nil {
+		log.Println(err)
+		return nil, err
+	}
+	return b, nil
+}
+
+func (c connectionCtx) updateStatus() {
+	if stStr, err := c.bm.GetStatus(); err != nil {
+		log.Printf("Error getting status: %v", err)
+	} else {
+		setStatusFromStr(stStr)
+	}
+
+	statusCh := c.bm.GetStatusCh()
+	for {
+		select {
+		case stStr := <-statusCh:
+			setStatusFromStr(stStr)
+		}
+	}
+}
+
+var ctx *connectionCtx
+
+func setStatus(st status) {
+	stmut.Lock()
+	defer stmut.Unlock()
+	ctx.Status = st
+	go trigger(OnStatusChanged)
+}
+
+func setStatusFromStr(stStr string) {
+	log.Println("status:", stStr)
+	setStatus(unknown.fromString(stStr))
+}
+
+func initPrinter() *message.Printer {
+	locale, err := go_locale.DetectLocale()
+	if err != nil {
+		log.Println("Error detecting the system locale: ", err)
+	}
+
+	return message.NewPrinter(message.MatchLanguage(locale, "en"))
+}
+
+// initializeBitmask instantiates a bitmask connection
+func initializeBitmask() {
+	if ctx == nil {
+		log.Println("error: cannot initialize bitmask, ctx is nil")
+		os.Exit(1)
+	}
+	conf := systray.ParseConfig()
+	conf.Version = "unknown"
+	conf.Printer = initPrinter()
+	b, err := bitmask.Init(conf.Printer)
+	if err != nil {
+		log.Fatal(err)
+	}
+	ctx.bm = b
+}
+
+func startVPN() {
+	err := ctx.bm.StartVPN(ctx.Provider)
+	if err != nil {
+		log.Println(err)
+		os.Exit(1)
+	}
+}
+
+func stopVPN() {
+	err := ctx.bm.StopVPN()
+	if err != nil {
+		log.Println(err)
+	}
+}
+
+// initializeContext initializes an empty connStatus and assigns it to the
+// global ctx holder. This is expected to be called only once, so the public
+// api uses the sync.Once primitive to call this.
+func initializeContext(provider, appName string) {
+	var st status = off
+	ctx = &connectionCtx{
+		AppName:  appName,
+		Provider: provider,
+		Status:   st,
+	}
+	go trigger(OnStatusChanged)
+	initializeBitmask()
+}
+
+/* mock http server: easy way to mocking vpn behavior on ui interaction. This
+* should also show a good way of writing functionality tests just for the Qml
+* layer */
+
+func mockUIOn(w http.ResponseWriter, r *http.Request) {
+	log.Println("changing status: on")
+	setStatus(on)
+}
+
+func mockUIOff(w http.ResponseWriter, r *http.Request) {
+	log.Println("changing status: off")
+	setStatus(off)
+}
+
+func mockUIFailed(w http.ResponseWriter, r *http.Request) {
+	log.Println("changing status: failed")
+	setStatus(failed)
+}
+
+func mockUI() {
+	http.HandleFunc("/on", mockUIOn)
+	http.HandleFunc("/off", mockUIOff)
+	http.HandleFunc("/failed", mockUIFailed)
+	http.ListenAndServe(":8080", nil)
+}
+
+/*
+
+  exported C api
+
+*/
+
+//export SwitchOn
+func SwitchOn() {
+	setStatus(starting)
+	startVPN()
+}
+
+//export SwitchOff
+func SwitchOff() {
+	setStatus(stopping)
+	stopVPN()
+}
+
+//export Quit
+func Quit() {
+	if ctx.Status != off {
+		setStatus(stopping)
+		stopVPN()
+	}
+}
+
+//export Unblock
+func Unblock() {
+	fmt.Println("unblock... [not implemented]")
+}
+
+//export SubscribeToEvent
+func SubscribeToEvent(event string, f unsafe.Pointer) {
+	subscribe(event, f)
+}
+
+//export InitializeBitmaskContext
+func InitializeBitmaskContext() {
+	provider := "black.riseup.net"
+	appName := "RiseupVPN"
+	initOnce.Do(func() {
+		initializeContext(provider, appName)
+	})
+	go ctx.updateStatus()
+}
+
+//export RefreshContext
+func RefreshContext() *C.char {
+	c, _ := ctx.toJson()
+	return C.CString(string(c))
+}
+
+/* end of the exposed api */
+
+/* we could enable this one optionally for the qt tests */
+
+/* uncomment: export MockUIInteraction */
+func MockUIInteraction() {
+	log.Println("mocking ui interaction on port 8080. \nTry 'curl localhost:8080/{on|off|failed}' to toggle status.")
+	go mockUI()
+}
+
+func main() {}
-- 
GitLab