Skip to content
Snippets Groups Projects
Unverified Commit 3b6e3d9b authored by Kali Kaneko's avatar Kali Kaneko
Browse files

[feat] add go wrapper

parent 9b88f3ce
No related branches found
No related tags found
No related merge requests found
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
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() {}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment