diff options
-rw-r--r-- | bitmask.pro | 72 | ||||
-rw-r--r-- | gui/backend.go | 342 |
2 files changed, 414 insertions, 0 deletions
diff --git a/bitmask.pro b/bitmask.pro new file mode 100644 index 0000000..1afe162 --- /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 0000000..9a952a7 --- /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() {} |