From 0ac0afaaf312a02af01d1c307ecf9b5915f40b0d Mon Sep 17 00:00:00 2001 From: "kali kaneko (leap communications)" Date: Fri, 12 Jun 2020 20:00:13 +0200 Subject: [refactor] reorganize backend in its own module Signed-off-by: kali kaneko (leap communications) --- pkg/backend/api.go | 74 ++++++++++++++++++++++++++++ pkg/backend/bitmask.go | 52 ++++++++++++++++++++ pkg/backend/callbacks.go | 63 ++++++++++++++++++++++++ pkg/backend/mocks.go | 32 +++++++++++++ pkg/backend/status.go | 122 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 343 insertions(+) create mode 100644 pkg/backend/api.go create mode 100644 pkg/backend/bitmask.go create mode 100644 pkg/backend/callbacks.go create mode 100644 pkg/backend/mocks.go create mode 100644 pkg/backend/status.go (limited to 'pkg/backend') diff --git a/pkg/backend/api.go b/pkg/backend/api.go new file mode 100644 index 0000000..f924cbd --- /dev/null +++ b/pkg/backend/api.go @@ -0,0 +1,74 @@ +/* All the exported functions live here */ + +package backend + +import ( + "C" + "fmt" + "log" + "unsafe" + + "0xacab.org/leap/bitmask-vpn/pkg/bitmask" + "0xacab.org/leap/bitmask-vpn/pkg/pickle" +) + +func SwitchOn() { + go setStatus(starting) + go startVPN() +} + +func SwitchOff() { + go setStatus(stopping) + go stopVPN() +} + +func Unblock() { + fmt.Println("unblock... [not implemented]") +} + +func Quit() { + if ctx.Status != off { + go setStatus(stopping) + stopVPN() + } +} + +func ToggleDonate() { + toggleDonate() +} + +func SubscribeToEvent(event string, f unsafe.Pointer) { + subscribe(event, f) +} + +func InitializeBitmaskContext() { + pi := bitmask.GetConfiguredProvider() + + initOnce.Do(func() { + initializeContext(pi.Provider, pi.AppName) + }) + go ctx.updateStatus() + + /* DEBUG + timer := time.NewTimer(time.Second * 3) + go func() { + <-timer.C + fmt.Println("donate timer fired") + toggleDonate() + }() + */ +} + +func RefreshContext() *C.char { + c, _ := ctx.toJson() + return C.CString(string(c)) +} + +func InstallHelpers() { + pickle.InstallHelpers() +} + +func MockUIInteraction() { + log.Println("mocking ui interaction on port 8080. \nTry 'curl localhost:8080/{on|off|failed}' to toggle status.") + go mockUI() +} diff --git a/pkg/backend/bitmask.go b/pkg/backend/bitmask.go new file mode 100644 index 0000000..07d27ea --- /dev/null +++ b/pkg/backend/bitmask.go @@ -0,0 +1,52 @@ +package backend + +import ( + "log" + "os" + + "0xacab.org/leap/bitmask-vpn/pkg/bitmask" +) + +func initializeBitmask() { + if ctx == nil { + log.Println("error: cannot initialize bitmask, ctx is nil") + os.Exit(1) + } + bitmask.InitializeLogger() + + b, err := bitmask.InitializeBitmask() + if err != nil { + log.Println("error: cannot initialize bitmask") + } + 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, + Donate: false, + Status: st, + } + go trigger(OnStatusChanged) + initializeBitmask() +} diff --git a/pkg/backend/callbacks.go b/pkg/backend/callbacks.go new file mode 100644 index 0000000..5ea3b04 --- /dev/null +++ b/pkg/backend/callbacks.go @@ -0,0 +1,63 @@ +package backend + +import ( + "fmt" + "reflect" + "sync" + "unsafe" +) + +/* NOTE! ATCHUNG! what follow are not silly comments. Well, *this one* is, but + the lines after this are not. + Those are inline C functions, that are invoked by CGO later on. + it's also crucial that you don't any extra space between the function + block and the 'import "C"' line. */ + +// 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) + } +} diff --git a/pkg/backend/mocks.go b/pkg/backend/mocks.go new file mode 100644 index 0000000..a8ede73 --- /dev/null +++ b/pkg/backend/mocks.go @@ -0,0 +1,32 @@ +package backend + +import ( + "log" + "net/http" +) + +/* 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) +} diff --git a/pkg/backend/status.go b/pkg/backend/status.go new file mode 100644 index 0000000..e2d31db --- /dev/null +++ b/pkg/backend/status.go @@ -0,0 +1,122 @@ +package backend + +import ( + "bytes" + "encoding/json" + "log" + + "0xacab.org/leap/bitmask-vpn/pkg/bitmask" +) + +const ( + offStr = "off" + startingStr = "starting" + onStr = "on" + stoppingStr = "stopping" + failedStr = "failed" +) + +// ctx will be our glorious global object. +// if we ever switch again to a provider-agnostic app, we should keep a map here. +var ctx *connectionCtx + +// the status type reflects the current VPN status. Go code is responsible for updating +// it; the C gui 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 + } +} + +// 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"` + Donate bool `json:"donate"` + Status status `json:"status"` + 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) + } + } +} + +func setStatus(st status) { + stmut.Lock() + defer stmut.Unlock() + ctx.Status = st + go trigger(OnStatusChanged) +} + +func toggleDonate() { + stmut.Lock() + defer stmut.Unlock() + ctx.Donate = !ctx.Donate + go trigger(OnStatusChanged) +} + +func setStatusFromStr(stStr string) { + setStatus(unknown.fromString(stStr)) +} -- cgit v1.2.3